@rallycry/conveyor-agent 11.0.3 → 11.0.4
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-7P6QZHXZ.js +76 -0
- package/dist/{chunk-DHKAY7HQ.js → chunk-HYPX6FEX.js} +356 -147
- package/dist/{chunk-JQVAWRVL.js → chunk-URYBO32C.js} +40 -6
- package/dist/cli.js +25 -17
- package/dist/gate-cli.d.ts +1 -0
- package/dist/gate-cli.js +85 -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/package.json +3 -2
- package/skills/conveyor-build/SKILL.md +10 -0
- package/dist/{boot-PKQ2I66D.js → boot-SWUVT5B4.js} +5 -5
- package/dist/{chunk-N4WSUTGV.js → chunk-SLUBOIYE.js} +6 -6
|
@@ -2,6 +2,32 @@ 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
|
+
inheritedEnv,
|
|
13
|
+
killPtyWithEscalation,
|
|
14
|
+
needsRawReadyGate,
|
|
15
|
+
parseUserQuestions,
|
|
16
|
+
renderPromptContentText,
|
|
17
|
+
resolveClaudeBinary,
|
|
18
|
+
resolvePlanDialogTiming,
|
|
19
|
+
resolvePtySpawn,
|
|
20
|
+
resolveRawTuiProbeTiming,
|
|
21
|
+
resolveSubmitNudgeTiming,
|
|
22
|
+
resolveSubmitRedeliveryMaxAttempts,
|
|
23
|
+
resolveSubmitSettleMs,
|
|
24
|
+
sawTerminalSetup,
|
|
25
|
+
sentinelEchoed,
|
|
26
|
+
sessionTempBase,
|
|
27
|
+
spawnOptionsFingerprint,
|
|
28
|
+
transcriptSize,
|
|
29
|
+
turnOptionsFrom
|
|
30
|
+
} from "./chunk-3F4ZZKCA.js";
|
|
5
31
|
import {
|
|
6
32
|
AgentConnection,
|
|
7
33
|
CodespacePortVisibility,
|
|
@@ -30,11 +56,21 @@ import {
|
|
|
30
56
|
statWorkspacePath,
|
|
31
57
|
updateRemoteToken,
|
|
32
58
|
verifyGitCredential
|
|
33
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-SLUBOIYE.js";
|
|
34
60
|
import {
|
|
35
61
|
registerBootMilestoneSocketFallback,
|
|
36
62
|
reportBootMilestone
|
|
37
63
|
} from "./chunk-GL2DIQEQ.js";
|
|
64
|
+
import {
|
|
65
|
+
describeTokenFile,
|
|
66
|
+
ghHostsExternallyOwned,
|
|
67
|
+
githubTokenFilePath,
|
|
68
|
+
sleep
|
|
69
|
+
} from "./chunk-W4LZ7R6Z.js";
|
|
70
|
+
import {
|
|
71
|
+
isHeavyGateActive,
|
|
72
|
+
listGateExitSentinels
|
|
73
|
+
} from "./chunk-7P6QZHXZ.js";
|
|
38
74
|
import {
|
|
39
75
|
LoopLagMonitor,
|
|
40
76
|
loopStatusForRunnerStatus
|
|
@@ -45,38 +81,6 @@ import {
|
|
|
45
81
|
import {
|
|
46
82
|
workbenchEnabled
|
|
47
83
|
} 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
84
|
|
|
81
85
|
// ../shared/dist/chunk-OKJPFFQI.js
|
|
82
86
|
var CARD_DESCRIPTION_MAX = 255;
|
|
@@ -3134,8 +3138,8 @@ function mapTranscriptRecord(raw) {
|
|
|
3134
3138
|
// src/harness/pty/jsonl-tailer.ts
|
|
3135
3139
|
var POLL_INTERVAL_MS = 25;
|
|
3136
3140
|
var JsonlTailer = class {
|
|
3137
|
-
constructor(
|
|
3138
|
-
this.path =
|
|
3141
|
+
constructor(path, onEvent, onRawRecord, mapRecord = mapTranscriptRecord) {
|
|
3142
|
+
this.path = path;
|
|
3139
3143
|
this.onEvent = onEvent;
|
|
3140
3144
|
this.onRawRecord = onRawRecord;
|
|
3141
3145
|
this.mapRecord = mapRecord;
|
|
@@ -3991,11 +3995,13 @@ function conveyorCredentialsMarkerPath() {
|
|
|
3991
3995
|
function tokenFingerprint(accessToken) {
|
|
3992
3996
|
return createHash("sha256").update(accessToken).digest("hex");
|
|
3993
3997
|
}
|
|
3994
|
-
function buildCredentialsMarker(accessToken, now, previousHash) {
|
|
3998
|
+
function buildCredentialsMarker(accessToken, now, previousHash, refreshToken, codingAgentKeyId) {
|
|
3995
3999
|
const accessTokenSha256 = tokenFingerprint(accessToken);
|
|
3996
4000
|
return JSON.stringify({
|
|
3997
4001
|
accessTokenSha256,
|
|
3998
4002
|
...previousHash && previousHash !== accessTokenSha256 ? { previousAccessTokenSha256: previousHash } : {},
|
|
4003
|
+
...refreshToken ? { refreshTokenSha256: tokenFingerprint(refreshToken) } : {},
|
|
4004
|
+
...codingAgentKeyId ? { codingAgentKeyId } : {},
|
|
3999
4005
|
writtenAt: now
|
|
4000
4006
|
});
|
|
4001
4007
|
}
|
|
@@ -4012,16 +4018,59 @@ function parseCredentialsMarker(raw) {
|
|
|
4012
4018
|
return [];
|
|
4013
4019
|
}
|
|
4014
4020
|
}
|
|
4021
|
+
function parseCredentialsRefreshMarker(raw) {
|
|
4022
|
+
if (!raw || raw.trim() === "") return null;
|
|
4023
|
+
try {
|
|
4024
|
+
const parsed = JSON.parse(raw);
|
|
4025
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4026
|
+
const hash = parsed.refreshTokenSha256;
|
|
4027
|
+
return typeof hash === "string" && hash.length > 0 ? hash : null;
|
|
4028
|
+
} catch {
|
|
4029
|
+
return null;
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
function parseCredentialsMarkerKeyId(raw) {
|
|
4033
|
+
if (!raw || raw.trim() === "") return null;
|
|
4034
|
+
try {
|
|
4035
|
+
const parsed = JSON.parse(raw);
|
|
4036
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4037
|
+
const keyId = parsed.codingAgentKeyId;
|
|
4038
|
+
return typeof keyId === "string" && keyId.length > 0 ? keyId : null;
|
|
4039
|
+
} catch {
|
|
4040
|
+
return null;
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
function credentialMarkerMatches(raw, accessToken, refreshToken, expectedKeyId) {
|
|
4044
|
+
if (expectedKeyId && parseCredentialsMarkerKeyId(raw) !== expectedKeyId) return false;
|
|
4045
|
+
if (accessToken && parseCredentialsMarker(raw).includes(tokenFingerprint(accessToken)))
|
|
4046
|
+
return true;
|
|
4047
|
+
if (!refreshToken) return false;
|
|
4048
|
+
return parseCredentialsRefreshMarker(raw) === tokenFingerprint(refreshToken);
|
|
4049
|
+
}
|
|
4050
|
+
function parseCredentialsIdentity(credentialsRaw, markerRaw, expectedKeyId) {
|
|
4051
|
+
if (!credentialsRaw) return null;
|
|
4052
|
+
try {
|
|
4053
|
+
const parsed = JSON.parse(credentialsRaw);
|
|
4054
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4055
|
+
const oauth = parsed.claudeAiOauth;
|
|
4056
|
+
if (typeof oauth !== "object" || oauth === null) return null;
|
|
4057
|
+
const record = oauth;
|
|
4058
|
+
const accessToken = typeof record.accessToken === "string" ? record.accessToken : null;
|
|
4059
|
+
const refreshToken = typeof record.refreshToken === "string" ? record.refreshToken : null;
|
|
4060
|
+
return {
|
|
4061
|
+
accessToken,
|
|
4062
|
+
hasRefreshToken: Boolean(refreshToken),
|
|
4063
|
+
isConveyorOwned: credentialMarkerMatches(markerRaw, accessToken, refreshToken, expectedKeyId)
|
|
4064
|
+
};
|
|
4065
|
+
} catch {
|
|
4066
|
+
return null;
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4015
4069
|
|
|
4016
|
-
// src/harness/pty/credentials.ts
|
|
4070
|
+
// src/harness/pty/credentials-plan.ts
|
|
4017
4071
|
var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
4018
4072
|
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
|
-
}
|
|
4073
|
+
var LEGACY_SCOPES = ["user:inference", "user:profile"];
|
|
4025
4074
|
function parseClaudeOauthEnv(blob) {
|
|
4026
4075
|
if (!blob) return null;
|
|
4027
4076
|
try {
|
|
@@ -4029,7 +4078,7 @@ function parseClaudeOauthEnv(blob) {
|
|
|
4029
4078
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4030
4079
|
const record = parsed;
|
|
4031
4080
|
if (typeof record.access !== "string" || record.access === "") return null;
|
|
4032
|
-
const scopes = Array.isArray(record.scopes) ? record.scopes.filter((
|
|
4081
|
+
const scopes = Array.isArray(record.scopes) ? record.scopes.filter((scope) => typeof scope === "string" && scope !== "") : [];
|
|
4033
4082
|
return {
|
|
4034
4083
|
access: record.access,
|
|
4035
4084
|
refresh: typeof record.refresh === "string" && record.refresh ? record.refresh : void 0,
|
|
@@ -4059,19 +4108,13 @@ function parseClaudeAiOauth(raw) {
|
|
|
4059
4108
|
return null;
|
|
4060
4109
|
}
|
|
4061
4110
|
}
|
|
4062
|
-
var LEGACY_SCOPES = ["user:inference", "user:profile"];
|
|
4063
4111
|
function buildCredentialsFile(material, now) {
|
|
4064
4112
|
const refresh = material.refresh;
|
|
4065
4113
|
return JSON.stringify({
|
|
4066
4114
|
claudeAiOauth: {
|
|
4067
4115
|
accessToken: material.access,
|
|
4068
4116
|
...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
4117
|
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
4118
|
...material.refreshExpires ? { refreshTokenExpiresAt: material.refreshExpires } : {},
|
|
4076
4119
|
scopes: material.scopes?.length ? material.scopes : LEGACY_SCOPES,
|
|
4077
4120
|
...material.rateLimitTier ? { rateLimitTier: material.rateLimitTier } : {},
|
|
@@ -4083,31 +4126,74 @@ function buildSynthesizedCredentials(token, now) {
|
|
|
4083
4126
|
return buildCredentialsFile({ access: token }, now);
|
|
4084
4127
|
}
|
|
4085
4128
|
function isConveyorOwnedCredentials(existing, markerHashes) {
|
|
4086
|
-
const hasRefresh = typeof existing.refreshToken === "string" && existing.refreshToken
|
|
4129
|
+
const hasRefresh = typeof existing.refreshToken === "string" && existing.refreshToken !== "";
|
|
4087
4130
|
if (!hasRefresh) return true;
|
|
4088
|
-
if (markerHashes.length === 0) return false;
|
|
4089
4131
|
if (typeof existing.accessToken !== "string") return false;
|
|
4090
4132
|
return markerHashes.includes(tokenFingerprint(existing.accessToken));
|
|
4091
4133
|
}
|
|
4134
|
+
function hasStableRefreshOwnership(existing, markerRaw) {
|
|
4135
|
+
return typeof existing.refreshToken === "string" && parseCredentialsRefreshMarker(markerRaw) === tokenFingerprint(existing.refreshToken);
|
|
4136
|
+
}
|
|
4137
|
+
function isCurrentCredential(existing, material, now) {
|
|
4138
|
+
if (material.refresh) return existing.accessToken === material.access;
|
|
4139
|
+
return existing.accessToken === material.access && typeof existing.expiresAt === "number" && existing.expiresAt > now + REFRESH_SKEW_MS;
|
|
4140
|
+
}
|
|
4141
|
+
function needsMarkerUpgrade(input, material) {
|
|
4142
|
+
if (!input.codingAgentKeyId) return false;
|
|
4143
|
+
if (parseCredentialsMarkerKeyId(input.markerRaw ?? null) !== input.codingAgentKeyId) return true;
|
|
4144
|
+
return Boolean(
|
|
4145
|
+
material.refresh && parseCredentialsRefreshMarker(input.markerRaw ?? null) !== tokenFingerprint(material.refresh)
|
|
4146
|
+
);
|
|
4147
|
+
}
|
|
4148
|
+
function ownsExistingCredential(existing, markerRaw, markerHashes) {
|
|
4149
|
+
if (isConveyorOwnedCredentials(existing, markerHashes)) return true;
|
|
4150
|
+
const accessToken = typeof existing.accessToken === "string" ? existing.accessToken : null;
|
|
4151
|
+
const refreshToken = typeof existing.refreshToken === "string" ? existing.refreshToken : null;
|
|
4152
|
+
return credentialMarkerMatches(markerRaw, accessToken, refreshToken);
|
|
4153
|
+
}
|
|
4154
|
+
function cliRefreshedSameKey(input, material, existing, markerRaw) {
|
|
4155
|
+
if (!material.refresh || !input.codingAgentKeyId) return false;
|
|
4156
|
+
if (parseCredentialsMarkerKeyId(markerRaw) !== input.codingAgentKeyId) return false;
|
|
4157
|
+
return hasStableRefreshOwnership(existing, markerRaw) && existing.accessToken !== material.access;
|
|
4158
|
+
}
|
|
4092
4159
|
function planCredentialsWrite(input) {
|
|
4093
4160
|
if (!input.isCloud) return { action: "skip", reason: "not-cloud" };
|
|
4094
4161
|
const material = parseClaudeOauthEnv(input.oauthBlob) ?? (input.token ? { access: input.token } : null);
|
|
4095
4162
|
if (!material) return { action: "skip", reason: "no-token" };
|
|
4096
|
-
const
|
|
4163
|
+
const markerRaw = input.markerRaw ?? null;
|
|
4164
|
+
const markerHashes = parseCredentialsMarker(markerRaw);
|
|
4097
4165
|
const contents = buildCredentialsFile(material, input.now);
|
|
4098
|
-
const marker = buildCredentialsMarker(
|
|
4166
|
+
const marker = buildCredentialsMarker(
|
|
4167
|
+
material.access,
|
|
4168
|
+
input.now,
|
|
4169
|
+
markerHashes[0] ?? null,
|
|
4170
|
+
material.refresh,
|
|
4171
|
+
input.codingAgentKeyId
|
|
4172
|
+
);
|
|
4099
4173
|
const existing = parseClaudeAiOauth(input.existingRaw);
|
|
4100
4174
|
if (!existing) return { action: "write", contents, marker };
|
|
4101
|
-
if (!
|
|
4175
|
+
if (!ownsExistingCredential(existing, markerRaw, markerHashes)) {
|
|
4102
4176
|
return { action: "skip", reason: "foreign-credentials" };
|
|
4103
4177
|
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4178
|
+
if (cliRefreshedSameKey(input, material, existing, markerRaw)) {
|
|
4179
|
+
return { action: "skip", reason: "current" };
|
|
4180
|
+
}
|
|
4181
|
+
if (!isCurrentCredential(existing, material, input.now)) {
|
|
4182
|
+
return { action: "write", contents, marker };
|
|
4183
|
+
}
|
|
4184
|
+
return needsMarkerUpgrade(input, material) ? { action: "mark", marker } : { action: "skip", reason: "current" };
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
// src/harness/pty/credentials.ts
|
|
4188
|
+
function claudeCredentialsPath() {
|
|
4189
|
+
return join5(claudeConfigHome(), ".credentials.json");
|
|
4190
|
+
}
|
|
4191
|
+
function isConveyorCloudEnv(env = process.env) {
|
|
4192
|
+
return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
|
|
4107
4193
|
}
|
|
4108
|
-
async function readRaw(
|
|
4194
|
+
async function readRaw(path) {
|
|
4109
4195
|
try {
|
|
4110
|
-
return await readFile(
|
|
4196
|
+
return await readFile(path, "utf8");
|
|
4111
4197
|
} catch {
|
|
4112
4198
|
return null;
|
|
4113
4199
|
}
|
|
@@ -4128,13 +4214,12 @@ async function resolveTuiAuthReadiness(env = process.env, readIdentity = readCre
|
|
|
4128
4214
|
});
|
|
4129
4215
|
return { ready: status === "ready", status };
|
|
4130
4216
|
}
|
|
4131
|
-
async function readCredentialsIdentity() {
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
};
|
|
4217
|
+
async function readCredentialsIdentity(env = process.env) {
|
|
4218
|
+
return parseCredentialsIdentity(
|
|
4219
|
+
await readRaw(claudeCredentialsPath()),
|
|
4220
|
+
await readRaw(conveyorCredentialsMarkerPath()),
|
|
4221
|
+
env.CONVEYOR_CODING_AGENT_KEY_ID
|
|
4222
|
+
);
|
|
4138
4223
|
}
|
|
4139
4224
|
var READ_BACK_DELAYS_MS = [250, 500, 1e3, 2e3];
|
|
4140
4225
|
var defaultSleep = (ms) => new Promise((resolve) => {
|
|
@@ -4149,10 +4234,10 @@ async function writeWithReadBackRetry(io, contents, delaysMs = READ_BACK_DELAYS_
|
|
|
4149
4234
|
await sleep2(delaysMs[attempt]);
|
|
4150
4235
|
}
|
|
4151
4236
|
}
|
|
4152
|
-
function fsWriteIo(
|
|
4237
|
+
function fsWriteIo(path, mode) {
|
|
4153
4238
|
return {
|
|
4154
|
-
write: (contents) => writeFile4(
|
|
4155
|
-
read: () => readRaw(
|
|
4239
|
+
write: (contents) => writeFile4(path, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
|
|
4240
|
+
read: () => readRaw(path)
|
|
4156
4241
|
};
|
|
4157
4242
|
}
|
|
4158
4243
|
async function ensureClaudeCredentials(env = process.env) {
|
|
@@ -4164,27 +4249,29 @@ async function ensureClaudeCredentials(env = process.env) {
|
|
|
4164
4249
|
await sanitizeApprovedApiKeys(accessToken);
|
|
4165
4250
|
}
|
|
4166
4251
|
try {
|
|
4167
|
-
const
|
|
4252
|
+
const path = claudeCredentialsPath();
|
|
4168
4253
|
const markerPath = conveyorCredentialsMarkerPath();
|
|
4169
4254
|
const plan = planCredentialsWrite({
|
|
4170
4255
|
isCloud,
|
|
4171
4256
|
token,
|
|
4172
4257
|
oauthBlob: env.CONVEYOR_CLAUDE_OAUTH,
|
|
4173
|
-
existingRaw: await readRaw(
|
|
4258
|
+
existingRaw: await readRaw(path),
|
|
4174
4259
|
markerRaw: await readRaw(markerPath),
|
|
4260
|
+
codingAgentKeyId: env.CONVEYOR_CODING_AGENT_KEY_ID,
|
|
4175
4261
|
now: Date.now()
|
|
4176
4262
|
});
|
|
4177
4263
|
if (plan.action === "skip") return;
|
|
4178
4264
|
await mkdir2(claudeConfigHome(), { recursive: true });
|
|
4179
4265
|
await writeWithReadBackRetry(fsWriteIo(markerPath, 384), plan.marker);
|
|
4180
|
-
|
|
4266
|
+
if (plan.action === "mark") return;
|
|
4267
|
+
const verified = await writeWithReadBackRetry(fsWriteIo(path, 384), plan.contents);
|
|
4181
4268
|
if (!verified) {
|
|
4182
4269
|
process.stderr.write(
|
|
4183
|
-
`[conveyor-agent] claude credentials read-back still stale after retries at ${
|
|
4270
|
+
`[conveyor-agent] claude credentials read-back still stale after retries at ${path} \u2014 TUI may land on the login picker
|
|
4184
4271
|
`
|
|
4185
4272
|
);
|
|
4186
4273
|
}
|
|
4187
|
-
await chmod2(
|
|
4274
|
+
await chmod2(path, 384).catch(() => {
|
|
4188
4275
|
});
|
|
4189
4276
|
} catch (err) {
|
|
4190
4277
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4213,12 +4300,12 @@ function planApprovedApiKeyCleanup(existingRaw, oauthToken) {
|
|
|
4213
4300
|
}
|
|
4214
4301
|
async function sanitizeApprovedApiKeys(oauthToken) {
|
|
4215
4302
|
try {
|
|
4216
|
-
const
|
|
4217
|
-
const cleaned = planApprovedApiKeyCleanup(await readRaw(
|
|
4303
|
+
const path = claudeJsonPath();
|
|
4304
|
+
const cleaned = planApprovedApiKeyCleanup(await readRaw(path), oauthToken);
|
|
4218
4305
|
if (cleaned === null) return;
|
|
4219
|
-
const verified = await writeWithReadBackRetry(fsWriteIo(
|
|
4306
|
+
const verified = await writeWithReadBackRetry(fsWriteIo(path), cleaned);
|
|
4220
4307
|
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 ${
|
|
4308
|
+
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
4309
|
`
|
|
4223
4310
|
);
|
|
4224
4311
|
} catch (err) {
|
|
@@ -4355,8 +4442,8 @@ async function persistOauthIdentityMarker(configIdentity, markerIdentity) {
|
|
|
4355
4442
|
async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
4356
4443
|
try {
|
|
4357
4444
|
if (!isConveyorCloudEnv(env)) return;
|
|
4358
|
-
const
|
|
4359
|
-
const existingRaw = await readRaw(
|
|
4445
|
+
const path = claudeJsonPath();
|
|
4446
|
+
const existingRaw = await readRaw(path);
|
|
4360
4447
|
const markerIdentity = parseOauthIdentity(await readRaw(conveyorOauthMarkerPath()));
|
|
4361
4448
|
await persistOauthIdentityMarker(
|
|
4362
4449
|
extractOauthIdentity(parseClaudeJson(existingRaw)),
|
|
@@ -4364,16 +4451,16 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
|
4364
4451
|
);
|
|
4365
4452
|
const contents = planClaudeJsonSeed(existingRaw, trustCwd, markerIdentity);
|
|
4366
4453
|
if (contents === null) return;
|
|
4367
|
-
const verified = await writeWithReadBackRetry(fsWriteIo(
|
|
4454
|
+
const verified = await writeWithReadBackRetry(fsWriteIo(path), contents);
|
|
4368
4455
|
if (verified) {
|
|
4369
4456
|
process.stderr.write(
|
|
4370
4457
|
`[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
|
|
4371
4458
|
`
|
|
4372
4459
|
);
|
|
4373
4460
|
} else {
|
|
4374
|
-
const verify = await readRaw(
|
|
4461
|
+
const verify = await readRaw(path);
|
|
4375
4462
|
process.stderr.write(
|
|
4376
|
-
`[conveyor-agent] claude onboarding seed read-back MISMATCH at ${
|
|
4463
|
+
`[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
4464
|
`
|
|
4378
4465
|
);
|
|
4379
4466
|
}
|
|
@@ -4386,13 +4473,13 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
|
4386
4473
|
async function removeConveyorCredentials(env = process.env) {
|
|
4387
4474
|
try {
|
|
4388
4475
|
if (!isConveyorCloudEnv(env)) return;
|
|
4389
|
-
const
|
|
4476
|
+
const path = claudeCredentialsPath();
|
|
4390
4477
|
const markerPath = conveyorCredentialsMarkerPath();
|
|
4391
|
-
const existing = parseClaudeAiOauth(await readRaw(
|
|
4478
|
+
const existing = parseClaudeAiOauth(await readRaw(path));
|
|
4392
4479
|
if (existing && !isConveyorOwnedCredentials(existing, parseCredentialsMarker(await readRaw(markerPath)))) {
|
|
4393
4480
|
return;
|
|
4394
4481
|
}
|
|
4395
|
-
await rm(
|
|
4482
|
+
await rm(path, { force: true });
|
|
4396
4483
|
await rm(markerPath, { force: true });
|
|
4397
4484
|
} catch (err) {
|
|
4398
4485
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -5901,10 +5988,10 @@ var TuiUnavailableError = class extends Error {
|
|
|
5901
5988
|
}
|
|
5902
5989
|
tui;
|
|
5903
5990
|
};
|
|
5904
|
-
function isExecutable(
|
|
5991
|
+
function isExecutable(path) {
|
|
5905
5992
|
try {
|
|
5906
|
-
if (!statSync(
|
|
5907
|
-
accessSync(
|
|
5993
|
+
if (!statSync(path).isFile()) return false;
|
|
5994
|
+
accessSync(path, constants.X_OK);
|
|
5908
5995
|
return true;
|
|
5909
5996
|
} catch {
|
|
5910
5997
|
return false;
|
|
@@ -5957,21 +6044,21 @@ function shouldSeed(existingEntry, seed) {
|
|
|
5957
6044
|
if (typeof entry.expires !== "number") return true;
|
|
5958
6045
|
return entry.expires < seed.expires;
|
|
5959
6046
|
}
|
|
5960
|
-
async function readJsonFile(
|
|
6047
|
+
async function readJsonFile(path) {
|
|
5961
6048
|
try {
|
|
5962
|
-
return JSON.parse(await fs.readFile(
|
|
6049
|
+
return JSON.parse(await fs.readFile(path, "utf8"));
|
|
5963
6050
|
} catch {
|
|
5964
6051
|
return {};
|
|
5965
6052
|
}
|
|
5966
6053
|
}
|
|
5967
|
-
async function writeJsonFile(
|
|
5968
|
-
await fs.mkdir(dirname2(
|
|
5969
|
-
await fs.writeFile(
|
|
6054
|
+
async function writeJsonFile(path, value) {
|
|
6055
|
+
await fs.mkdir(dirname2(path), { recursive: true });
|
|
6056
|
+
await fs.writeFile(path, `${JSON.stringify(value, null, 2)}
|
|
5970
6057
|
`, { mode: 384 });
|
|
5971
6058
|
}
|
|
5972
6059
|
async function ensureAuthEntry(env, seed) {
|
|
5973
|
-
const
|
|
5974
|
-
const store = await readJsonFile(
|
|
6060
|
+
const path = opencodeAuthPath(env);
|
|
6061
|
+
const store = await readJsonFile(path);
|
|
5975
6062
|
if (!shouldSeed(store.openai, seed)) {
|
|
5976
6063
|
logger.info("opencode oauth store is fresher than the seed; leaving it alone");
|
|
5977
6064
|
return;
|
|
@@ -5982,12 +6069,12 @@ async function ensureAuthEntry(env, seed) {
|
|
|
5982
6069
|
refresh: seed.refresh,
|
|
5983
6070
|
expires: seed.expires
|
|
5984
6071
|
};
|
|
5985
|
-
await writeJsonFile(
|
|
6072
|
+
await writeJsonFile(path, store);
|
|
5986
6073
|
logger.info("seeded opencode oauth store entry");
|
|
5987
6074
|
}
|
|
5988
6075
|
async function ensurePluginConfig(env) {
|
|
5989
|
-
const
|
|
5990
|
-
const config = await readJsonFile(
|
|
6076
|
+
const path = opencodeConfigPath(env);
|
|
6077
|
+
const config = await readJsonFile(path);
|
|
5991
6078
|
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
5992
6079
|
const isOurs = (p) => typeof p === "string" && (p === PLUGIN_PACKAGE || p.startsWith(`${PLUGIN_PACKAGE}@`));
|
|
5993
6080
|
const hasExactPin = plugins.includes(OPENCODE_CODEX_PLUGIN);
|
|
@@ -5995,7 +6082,7 @@ async function ensurePluginConfig(env) {
|
|
|
5995
6082
|
if (hasExactPin && !hasStalePin) return;
|
|
5996
6083
|
const kept = plugins.filter((p) => !isOurs(p));
|
|
5997
6084
|
config.plugin = [...kept, OPENCODE_CODEX_PLUGIN];
|
|
5998
|
-
await writeJsonFile(
|
|
6085
|
+
await writeJsonFile(path, config);
|
|
5999
6086
|
logger.info("ensured opencode codex-auth plugin in config");
|
|
6000
6087
|
}
|
|
6001
6088
|
async function seedOpenCodeOauth(env) {
|
|
@@ -6217,9 +6304,9 @@ var OpenCodeHeadlessHarness = class {
|
|
|
6217
6304
|
*/
|
|
6218
6305
|
async writeSystemPrompt(text) {
|
|
6219
6306
|
if (!text || text.trim() === "") return null;
|
|
6220
|
-
const
|
|
6221
|
-
await writeFile6(
|
|
6222
|
-
return
|
|
6307
|
+
const path = join10(this.tempDir, "conveyor-instructions.md");
|
|
6308
|
+
await writeFile6(path, text, "utf8");
|
|
6309
|
+
return path;
|
|
6223
6310
|
}
|
|
6224
6311
|
/**
|
|
6225
6312
|
* Parse one NDJSON line and push whatever it maps to. Returns assistant text so
|
|
@@ -6682,7 +6769,7 @@ function wrapBridgeWithDirectStream(inner, reporter, options = {}) {
|
|
|
6682
6769
|
|
|
6683
6770
|
// src/execution/query-executor.ts
|
|
6684
6771
|
import { createHash as createHash2 } from "crypto";
|
|
6685
|
-
import { existsSync, readFileSync as
|
|
6772
|
+
import { existsSync, readFileSync as readFileSync2, truncateSync } from "fs";
|
|
6686
6773
|
|
|
6687
6774
|
// src/execution/chat-instructions.ts
|
|
6688
6775
|
function buildChatInstructions(context, scenario, newMessages) {
|
|
@@ -10460,9 +10547,9 @@ Paste this into the PR description to show it inline:
|
|
|
10460
10547
|
${snippet}`;
|
|
10461
10548
|
}
|
|
10462
10549
|
function buildUploadAttachmentTool(connection, config) {
|
|
10463
|
-
return defineContractTool(uploadAttachmentContract, async ({ path
|
|
10550
|
+
return defineContractTool(uploadAttachmentContract, async ({ path, title, tags }) => {
|
|
10464
10551
|
try {
|
|
10465
|
-
const filePath = isAbsolute(
|
|
10552
|
+
const filePath = isAbsolute(path) ? path : join12(config.workspaceDir, path);
|
|
10466
10553
|
const mimeType = inferMimeType(filePath);
|
|
10467
10554
|
const info = await statWorkspacePath(filePath);
|
|
10468
10555
|
if (!info.isFile) {
|
|
@@ -12646,33 +12733,6 @@ function collectMissingProps(taskProps) {
|
|
|
12646
12733
|
return missing;
|
|
12647
12734
|
}
|
|
12648
12735
|
|
|
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
12736
|
// src/execution/tool-loop-tracker.ts
|
|
12677
12737
|
var REPEAT_INTERRUPT_THRESHOLD = 4;
|
|
12678
12738
|
var REPEAT_INTERRUPT_INTERVAL = 4;
|
|
@@ -13173,10 +13233,10 @@ function resolveSessionStart(lineageKey, cwd) {
|
|
|
13173
13233
|
}
|
|
13174
13234
|
return { sessionId: sessionUuid };
|
|
13175
13235
|
}
|
|
13176
|
-
function repairTornSessionFile(
|
|
13236
|
+
function repairTornSessionFile(path) {
|
|
13177
13237
|
try {
|
|
13178
|
-
if (!existsSync(
|
|
13179
|
-
const content =
|
|
13238
|
+
if (!existsSync(path)) return false;
|
|
13239
|
+
const content = readFileSync2(path, "utf8");
|
|
13180
13240
|
if (content.length === 0) return false;
|
|
13181
13241
|
let keepEnd = content.length;
|
|
13182
13242
|
if (!content.endsWith("\n")) {
|
|
@@ -13195,9 +13255,9 @@ function repairTornSessionFile(path2) {
|
|
|
13195
13255
|
keepEnd = prevNewline + 1;
|
|
13196
13256
|
}
|
|
13197
13257
|
if (keepEnd === content.length) return false;
|
|
13198
|
-
truncateSync(
|
|
13258
|
+
truncateSync(path, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
|
|
13199
13259
|
logger5.warn("Repaired torn transcript before resume", {
|
|
13200
|
-
path
|
|
13260
|
+
path,
|
|
13201
13261
|
trimmedBytes: content.length - keepEnd
|
|
13202
13262
|
});
|
|
13203
13263
|
return true;
|
|
@@ -14301,6 +14361,7 @@ var FIRST_SEND_MS = 5e3;
|
|
|
14301
14361
|
var RESEND_INTERVAL_MS = 4e3;
|
|
14302
14362
|
var MAX_SENDS = 5;
|
|
14303
14363
|
var RENDER_SETTLE_MS = 900;
|
|
14364
|
+
var EMPTY_MCP_CONFIG = JSON.stringify({ mcpServers: {} });
|
|
14304
14365
|
function buildProbeEnv(env = process.env) {
|
|
14305
14366
|
const clean = {};
|
|
14306
14367
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -14356,7 +14417,7 @@ var UsageProbeRun = class {
|
|
|
14356
14417
|
this.settleTimer = setTimeout(() => this.finish(this.buf), this.timing.settleMs);
|
|
14357
14418
|
}
|
|
14358
14419
|
finishBestEffort() {
|
|
14359
|
-
this.finish(panelRendering(this.buf) ? this.buf :
|
|
14420
|
+
this.finish(panelRendering(this.buf) ? this.buf : cleanTerminalOutput(this.buf, 500));
|
|
14360
14421
|
}
|
|
14361
14422
|
finish(out) {
|
|
14362
14423
|
if (this.settled) return;
|
|
@@ -14368,6 +14429,12 @@ var UsageProbeRun = class {
|
|
|
14368
14429
|
this.resolve(out);
|
|
14369
14430
|
}
|
|
14370
14431
|
};
|
|
14432
|
+
function buildUsageProbeArgs() {
|
|
14433
|
+
return ["--mcp-config", EMPTY_MCP_CONFIG, "--strict-mcp-config"];
|
|
14434
|
+
}
|
|
14435
|
+
function resolveUsageProbeCwd(explicitCwd, env = process.env) {
|
|
14436
|
+
return explicitCwd ?? env.CONVEYOR_WORKSPACE ?? process.cwd();
|
|
14437
|
+
}
|
|
14371
14438
|
async function runUsageProbe(deps = {}) {
|
|
14372
14439
|
let spawn2 = deps.spawn;
|
|
14373
14440
|
if (!spawn2) {
|
|
@@ -14378,7 +14445,8 @@ async function runUsageProbe(deps = {}) {
|
|
|
14378
14445
|
}
|
|
14379
14446
|
}
|
|
14380
14447
|
const binary = deps.binary ?? resolveClaudeBinary();
|
|
14381
|
-
const
|
|
14448
|
+
const baseEnv = deps.env ?? process.env;
|
|
14449
|
+
const cwd = resolveUsageProbeCwd(deps.cwd, baseEnv);
|
|
14382
14450
|
const timeoutMs = deps.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
14383
14451
|
const timing = {
|
|
14384
14452
|
firstSendMs: deps.firstSendMs ?? FIRST_SEND_MS,
|
|
@@ -14388,12 +14456,12 @@ async function runUsageProbe(deps = {}) {
|
|
|
14388
14456
|
return new Promise((resolve) => {
|
|
14389
14457
|
let child;
|
|
14390
14458
|
try {
|
|
14391
|
-
child = spawn2(binary,
|
|
14459
|
+
child = spawn2(binary, buildUsageProbeArgs(), {
|
|
14392
14460
|
name: "xterm-256color",
|
|
14393
14461
|
cols: 120,
|
|
14394
14462
|
rows: 45,
|
|
14395
14463
|
cwd,
|
|
14396
|
-
env: buildProbeEnv(
|
|
14464
|
+
env: buildProbeEnv(baseEnv)
|
|
14397
14465
|
});
|
|
14398
14466
|
} catch {
|
|
14399
14467
|
resolve("");
|
|
@@ -14408,10 +14476,10 @@ var logger7 = createServiceLogger("usage-sampler");
|
|
|
14408
14476
|
var NO_SAMPLES = { samples: [], unmeasurable: null };
|
|
14409
14477
|
function isAttributable(identity, sessionToken) {
|
|
14410
14478
|
if (!identity) return { ok: true };
|
|
14411
|
-
if (identity.hasRefreshToken) {
|
|
14479
|
+
if (identity.hasRefreshToken && !identity.isConveyorOwned) {
|
|
14412
14480
|
return { ok: false, reason: "manual-login-credentials" };
|
|
14413
14481
|
}
|
|
14414
|
-
if (sessionToken && identity.accessToken && identity.accessToken !== sessionToken) {
|
|
14482
|
+
if (!(identity.hasRefreshToken && identity.isConveyorOwned) && sessionToken && identity.accessToken && identity.accessToken !== sessionToken) {
|
|
14415
14483
|
return { ok: false, reason: "credentials-token-mismatch" };
|
|
14416
14484
|
}
|
|
14417
14485
|
return { ok: true };
|
|
@@ -14524,6 +14592,9 @@ async function handlePullBranch(workDir, branch) {
|
|
|
14524
14592
|
}
|
|
14525
14593
|
}
|
|
14526
14594
|
|
|
14595
|
+
// src/runner/session-runner.ts
|
|
14596
|
+
import { rmSync } from "fs";
|
|
14597
|
+
|
|
14527
14598
|
// src/runner/background-work.ts
|
|
14528
14599
|
var BACKGROUND_WORK_MAX_MS = 45 * 60 * 1e3;
|
|
14529
14600
|
var BACKGROUND_DEFAULT_BY_TOOL = {
|
|
@@ -14635,6 +14706,7 @@ function findLiveChild(sources) {
|
|
|
14635
14706
|
// src/runner/session-runner.ts
|
|
14636
14707
|
var AUTO_RUN_MODES = /* @__PURE__ */ new Set(["building", "auto", "review", "discovery", "chat"]);
|
|
14637
14708
|
var CHILD_DEFER_RECHECK_MS = 60 * 1e3;
|
|
14709
|
+
var ORPHAN_WAKE_GRACE_MS = 3 * 60 * 1e3;
|
|
14638
14710
|
var SessionRunner = class _SessionRunner {
|
|
14639
14711
|
connection;
|
|
14640
14712
|
mode;
|
|
@@ -14704,9 +14776,11 @@ var SessionRunner = class _SessionRunner {
|
|
|
14704
14776
|
onHeartbeat: () => {
|
|
14705
14777
|
const loopStatus = this.refreshLoopStatus();
|
|
14706
14778
|
this.connection.sendHeartbeat(this.loopLag.takeMaxLagMs(), loopStatus);
|
|
14779
|
+
this.maybeWakeForOrphanedBackgroundWork();
|
|
14707
14780
|
},
|
|
14708
14781
|
onIdleTimeout: () => {
|
|
14709
14782
|
if (this.deferShutdownForLiveChild("idle")) return;
|
|
14783
|
+
if (this.deferShutdownForBackgroundWork()) return;
|
|
14710
14784
|
process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
|
|
14711
14785
|
this.stopped = true;
|
|
14712
14786
|
this.queryBridge?.stop();
|
|
@@ -14787,6 +14861,133 @@ var SessionRunner = class _SessionRunner {
|
|
|
14787
14861
|
}
|
|
14788
14862
|
return true;
|
|
14789
14863
|
}
|
|
14864
|
+
/** Epoch ms of the first idle-shutdown deferral for background work in the
|
|
14865
|
+
* current idle episode; null when nothing has deferred. Reset when a turn
|
|
14866
|
+
* runs, so each idle episode gets its own bounded hold. */
|
|
14867
|
+
backgroundDeferStartedAt = null;
|
|
14868
|
+
/**
|
|
14869
|
+
* Suppress an idle shutdown while backgrounded work (`run_in_background`
|
|
14870
|
+
* launches tracked by `BackgroundWorkTracker`) or a live gate process
|
|
14871
|
+
* (`isHeavyGateActive` — singleton pidfiles plus the `conveyor-gate`
|
|
14872
|
+
* contract dir) is still running. Without this the 30-minute idle timeout
|
|
14873
|
+
* stopped the runner mid-gate: `shutdown()` cleared the tracker, heartbeats
|
|
14874
|
+
* stopped, the activity clock expired, and the reconciler slept the pod and
|
|
14875
|
+
* reverted the card to Open.
|
|
14876
|
+
*
|
|
14877
|
+
* Bounded: deferrals past the first one cap at `BACKGROUND_WORK_MAX_MS`, so
|
|
14878
|
+
* a wedged gate process cannot pin a pod forever. Only the IDLE timeout is
|
|
14879
|
+
* deferred — post-`completed` dormancy keeps today's behavior, and a parked
|
|
14880
|
+
* prefill (`waiting_for_input`) contributes nothing here, so those cards
|
|
14881
|
+
* still sleep on the ordinary window.
|
|
14882
|
+
*/
|
|
14883
|
+
deferShutdownForBackgroundWork() {
|
|
14884
|
+
let holding = null;
|
|
14885
|
+
try {
|
|
14886
|
+
if (this.backgroundWork.hasPending()) holding = "background work outstanding";
|
|
14887
|
+
else if (isHeavyGateActive()) holding = "a gate process is running";
|
|
14888
|
+
} catch (err) {
|
|
14889
|
+
process.stderr.write(`[conveyor-agent] Background-work probe failed, not deferring: ${err}
|
|
14890
|
+
`);
|
|
14891
|
+
return false;
|
|
14892
|
+
}
|
|
14893
|
+
if (!holding) {
|
|
14894
|
+
this.backgroundDeferStartedAt = null;
|
|
14895
|
+
return false;
|
|
14896
|
+
}
|
|
14897
|
+
const now = Date.now();
|
|
14898
|
+
this.backgroundDeferStartedAt ??= now;
|
|
14899
|
+
if (now - this.backgroundDeferStartedAt >= BACKGROUND_WORK_MAX_MS) {
|
|
14900
|
+
process.stderr.write(
|
|
14901
|
+
`[conveyor-agent] Idle-shutdown deferral cap (${BACKGROUND_WORK_MAX_MS}ms) reached while ${holding} \u2014 proceeding with shutdown
|
|
14902
|
+
`
|
|
14903
|
+
);
|
|
14904
|
+
return false;
|
|
14905
|
+
}
|
|
14906
|
+
const recheckMs = Math.min(this.lifecycle.config.idleTimeoutMs, CHILD_DEFER_RECHECK_MS);
|
|
14907
|
+
process.stderr.write(`[conveyor-agent] Idle timeout deferred: ${holding}
|
|
14908
|
+
`);
|
|
14909
|
+
this.lifecycle.startIdleTimer(recheckMs);
|
|
14910
|
+
return true;
|
|
14911
|
+
}
|
|
14912
|
+
/** When orphan evidence (tracked work pending, no live gate process) began
|
|
14913
|
+
* holding continuously; null while the evidence is absent. */
|
|
14914
|
+
orphanGraceStartedAt = null;
|
|
14915
|
+
/** One-shot latch: at most one watchdog wake per idle episode. Reset when a
|
|
14916
|
+
* turn runs. */
|
|
14917
|
+
backgroundWakeFiredThisIdle = false;
|
|
14918
|
+
/** When the current idle episode began — an exit sentinel older than this
|
|
14919
|
+
* appeared while a turn could still have consumed it, so it never wakes. */
|
|
14920
|
+
idleEpisodeStartedAt = 0;
|
|
14921
|
+
/**
|
|
14922
|
+
* Wake a parked agent whose background work finished — or died — without a
|
|
14923
|
+
* consumed completion callback. Completion normally arrives as the CLI's
|
|
14924
|
+
* `<task-notification>` transcript record; when that record is lost (SDK
|
|
14925
|
+
* harness, torn transcript, a gate killed out from under the CLI) the
|
|
14926
|
+
* tracker entry silently expires and the agent waits forever.
|
|
14927
|
+
*
|
|
14928
|
+
* This is NOT the removed auto-mode stuck-nudge: it fires only on positive
|
|
14929
|
+
* evidence of orphaned background work — a tracked launch with no live gate
|
|
14930
|
+
* process continuously past `ORPHAN_WAKE_GRACE_MS`, or an unconsumed
|
|
14931
|
+
* `gates/<label>.exit` sentinel that appeared this idle episode — never on
|
|
14932
|
+
* "the agent seems stuck". A `completed` agent is never woken (the
|
|
14933
|
+
* completion guard holds), and a live pid always suppresses the wake.
|
|
14934
|
+
*/
|
|
14935
|
+
maybeWakeForOrphanedBackgroundWork() {
|
|
14936
|
+
try {
|
|
14937
|
+
if (this._state !== "idle" || this.stopped || this.completedThisTurn) {
|
|
14938
|
+
this.orphanGraceStartedAt = null;
|
|
14939
|
+
return;
|
|
14940
|
+
}
|
|
14941
|
+
if (this.backgroundWakeFiredThisIdle) return;
|
|
14942
|
+
if (!this.inputResolver) return;
|
|
14943
|
+
if (isHeavyGateActive()) {
|
|
14944
|
+
this.orphanGraceStartedAt = null;
|
|
14945
|
+
return;
|
|
14946
|
+
}
|
|
14947
|
+
const evidence = this.findOrphanEvidence(Date.now());
|
|
14948
|
+
if (!evidence) return;
|
|
14949
|
+
process.stderr.write(
|
|
14950
|
+
`[conveyor-agent] Background-work watchdog: waking agent \u2014 ${evidence.description}
|
|
14951
|
+
`
|
|
14952
|
+
);
|
|
14953
|
+
this.backgroundWakeFiredThisIdle = true;
|
|
14954
|
+
this.orphanGraceStartedAt = null;
|
|
14955
|
+
if (evidence.sentinel) rmSync(evidence.sentinel.path, { force: true });
|
|
14956
|
+
this.backgroundWork.clear();
|
|
14957
|
+
const resolver = this.inputResolver;
|
|
14958
|
+
this.inputResolver = null;
|
|
14959
|
+
resolver({
|
|
14960
|
+
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.`,
|
|
14961
|
+
userId: "system",
|
|
14962
|
+
source: "background_work_check"
|
|
14963
|
+
});
|
|
14964
|
+
} catch (err) {
|
|
14965
|
+
process.stderr.write(`[conveyor-agent] Background-work watchdog probe failed: ${err}
|
|
14966
|
+
`);
|
|
14967
|
+
}
|
|
14968
|
+
}
|
|
14969
|
+
/** Orphan evidence that has held past the grace, or null. Maintains the
|
|
14970
|
+
* grace clock for the tracked-work condition as a side effect. */
|
|
14971
|
+
findOrphanEvidence(now) {
|
|
14972
|
+
const sentinel = listGateExitSentinels().find(
|
|
14973
|
+
(s) => s.mtimeMs >= this.idleEpisodeStartedAt && now - s.mtimeMs >= ORPHAN_WAKE_GRACE_MS
|
|
14974
|
+
);
|
|
14975
|
+
if (sentinel) {
|
|
14976
|
+
return {
|
|
14977
|
+
sentinel,
|
|
14978
|
+
description: `gate '${sentinel.label}' finished (exit ${sentinel.code ?? "unknown"}) with its completion never consumed`
|
|
14979
|
+
};
|
|
14980
|
+
}
|
|
14981
|
+
if (!this.backgroundWork.hasPending()) {
|
|
14982
|
+
this.orphanGraceStartedAt = null;
|
|
14983
|
+
return null;
|
|
14984
|
+
}
|
|
14985
|
+
this.orphanGraceStartedAt ??= now;
|
|
14986
|
+
if (now - this.orphanGraceStartedAt < ORPHAN_WAKE_GRACE_MS) return null;
|
|
14987
|
+
return {
|
|
14988
|
+
description: `${this.backgroundWork.pendingCount()} tracked background launch(es) with no live gate process for ${Math.round(ORPHAN_WAKE_GRACE_MS / 6e4)}+ minutes`
|
|
14989
|
+
};
|
|
14990
|
+
}
|
|
14790
14991
|
get sessionId() {
|
|
14791
14992
|
return this.connection.sessionId;
|
|
14792
14993
|
}
|
|
@@ -15396,7 +15597,10 @@ var SessionRunner = class _SessionRunner {
|
|
|
15396
15597
|
async sampleAndReportKeyUsage() {
|
|
15397
15598
|
if (this.stopped) return;
|
|
15398
15599
|
const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
|
|
15399
|
-
const { samples, unmeasurable } = await sampleKeyUsage(
|
|
15600
|
+
const { samples, unmeasurable } = await sampleKeyUsage(
|
|
15601
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
15602
|
+
() => runUsageProbe({ cwd: this.config.workspaceDir })
|
|
15603
|
+
);
|
|
15400
15604
|
if (unmeasurable) {
|
|
15401
15605
|
this.connection.sendEvent(buildUnmeasurableEvent(unmeasurable.reason, codingAgentKeyId));
|
|
15402
15606
|
return;
|
|
@@ -15708,7 +15912,7 @@ ${outcome.failures.join("\n")}
|
|
|
15708
15912
|
*/
|
|
15709
15913
|
resolveLoopStatus() {
|
|
15710
15914
|
if (loopStatusForRunnerStatus(this._state) === "active") return "active";
|
|
15711
|
-
return this.backgroundWork.hasPending() ? "waiting" : "idle";
|
|
15915
|
+
return this.backgroundWork.hasPending() || isHeavyGateActive() ? "waiting" : "idle";
|
|
15712
15916
|
}
|
|
15713
15917
|
/** Mirror the resolved status into the loop-lag buffer so the starvation-proof
|
|
15714
15918
|
* heartbeat worker reports it too. Returns what it wrote. */
|
|
@@ -15724,6 +15928,11 @@ ${outcome.failures.join("\n")}
|
|
|
15724
15928
|
this.backgroundWork.noteToolUse(event.tool, event.input);
|
|
15725
15929
|
}
|
|
15726
15930
|
async setState(status) {
|
|
15931
|
+
if (status === "running") {
|
|
15932
|
+
this.backgroundDeferStartedAt = null;
|
|
15933
|
+
this.backgroundWakeFiredThisIdle = false;
|
|
15934
|
+
}
|
|
15935
|
+
if (status === "idle" && this._state !== "idle") this.idleEpisodeStartedAt = Date.now();
|
|
15727
15936
|
this._state = status;
|
|
15728
15937
|
this.refreshLoopStatus();
|
|
15729
15938
|
await this.connection.emitStatus(status);
|