@rallycry/conveyor-agent 11.0.2 → 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-QETNCXIR.js → chunk-HYPX6FEX.js} +410 -163
- 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 +83 -11
- 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" };
|
|
4107
4185
|
}
|
|
4108
|
-
|
|
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);
|
|
4193
|
+
}
|
|
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;
|
|
@@ -13150,7 +13210,9 @@ function taskIdToSessionUuid(lineageKey) {
|
|
|
13150
13210
|
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-8${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
|
|
13151
13211
|
}
|
|
13152
13212
|
function sessionLineageKey(taskId, agentMode, runnerMode) {
|
|
13153
|
-
|
|
13213
|
+
if (agentMode === "review" || runnerMode === "code-review") return `${taskId}:review`;
|
|
13214
|
+
if (runnerMode === "plan") return `${taskId}:plan`;
|
|
13215
|
+
return taskId;
|
|
13154
13216
|
}
|
|
13155
13217
|
function sessionFileExists(sessionUuid, cwd) {
|
|
13156
13218
|
try {
|
|
@@ -13171,10 +13233,10 @@ function resolveSessionStart(lineageKey, cwd) {
|
|
|
13171
13233
|
}
|
|
13172
13234
|
return { sessionId: sessionUuid };
|
|
13173
13235
|
}
|
|
13174
|
-
function repairTornSessionFile(
|
|
13236
|
+
function repairTornSessionFile(path) {
|
|
13175
13237
|
try {
|
|
13176
|
-
if (!existsSync(
|
|
13177
|
-
const content =
|
|
13238
|
+
if (!existsSync(path)) return false;
|
|
13239
|
+
const content = readFileSync2(path, "utf8");
|
|
13178
13240
|
if (content.length === 0) return false;
|
|
13179
13241
|
let keepEnd = content.length;
|
|
13180
13242
|
if (!content.endsWith("\n")) {
|
|
@@ -13193,9 +13255,9 @@ function repairTornSessionFile(path2) {
|
|
|
13193
13255
|
keepEnd = prevNewline + 1;
|
|
13194
13256
|
}
|
|
13195
13257
|
if (keepEnd === content.length) return false;
|
|
13196
|
-
truncateSync(
|
|
13258
|
+
truncateSync(path, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
|
|
13197
13259
|
logger5.warn("Repaired torn transcript before resume", {
|
|
13198
|
-
path
|
|
13260
|
+
path,
|
|
13199
13261
|
trimmedBytes: content.length - keepEnd
|
|
13200
13262
|
});
|
|
13201
13263
|
return true;
|
|
@@ -13417,7 +13479,7 @@ async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMou
|
|
|
13417
13479
|
host,
|
|
13418
13480
|
`[conveyor-agent] Turn produced no events for ${minutes}m with no heavy gate running \u2014 aborting the wedged turn
|
|
13419
13481
|
`,
|
|
13420
|
-
`Turn silent for ${minutes}m \u2014 aborting it so the agent can go idle
|
|
13482
|
+
`Turn silent for ${minutes}m \u2014 aborting it so the agent can go idle. A first turn retries automatically; otherwise the conversation resumes on the next message.`
|
|
13421
13483
|
);
|
|
13422
13484
|
return "keep_waiting";
|
|
13423
13485
|
}
|
|
@@ -14299,6 +14361,7 @@ var FIRST_SEND_MS = 5e3;
|
|
|
14299
14361
|
var RESEND_INTERVAL_MS = 4e3;
|
|
14300
14362
|
var MAX_SENDS = 5;
|
|
14301
14363
|
var RENDER_SETTLE_MS = 900;
|
|
14364
|
+
var EMPTY_MCP_CONFIG = JSON.stringify({ mcpServers: {} });
|
|
14302
14365
|
function buildProbeEnv(env = process.env) {
|
|
14303
14366
|
const clean = {};
|
|
14304
14367
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -14354,7 +14417,7 @@ var UsageProbeRun = class {
|
|
|
14354
14417
|
this.settleTimer = setTimeout(() => this.finish(this.buf), this.timing.settleMs);
|
|
14355
14418
|
}
|
|
14356
14419
|
finishBestEffort() {
|
|
14357
|
-
this.finish(panelRendering(this.buf) ? this.buf :
|
|
14420
|
+
this.finish(panelRendering(this.buf) ? this.buf : cleanTerminalOutput(this.buf, 500));
|
|
14358
14421
|
}
|
|
14359
14422
|
finish(out) {
|
|
14360
14423
|
if (this.settled) return;
|
|
@@ -14366,6 +14429,12 @@ var UsageProbeRun = class {
|
|
|
14366
14429
|
this.resolve(out);
|
|
14367
14430
|
}
|
|
14368
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
|
+
}
|
|
14369
14438
|
async function runUsageProbe(deps = {}) {
|
|
14370
14439
|
let spawn2 = deps.spawn;
|
|
14371
14440
|
if (!spawn2) {
|
|
@@ -14376,7 +14445,8 @@ async function runUsageProbe(deps = {}) {
|
|
|
14376
14445
|
}
|
|
14377
14446
|
}
|
|
14378
14447
|
const binary = deps.binary ?? resolveClaudeBinary();
|
|
14379
|
-
const
|
|
14448
|
+
const baseEnv = deps.env ?? process.env;
|
|
14449
|
+
const cwd = resolveUsageProbeCwd(deps.cwd, baseEnv);
|
|
14380
14450
|
const timeoutMs = deps.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
14381
14451
|
const timing = {
|
|
14382
14452
|
firstSendMs: deps.firstSendMs ?? FIRST_SEND_MS,
|
|
@@ -14386,12 +14456,12 @@ async function runUsageProbe(deps = {}) {
|
|
|
14386
14456
|
return new Promise((resolve) => {
|
|
14387
14457
|
let child;
|
|
14388
14458
|
try {
|
|
14389
|
-
child = spawn2(binary,
|
|
14459
|
+
child = spawn2(binary, buildUsageProbeArgs(), {
|
|
14390
14460
|
name: "xterm-256color",
|
|
14391
14461
|
cols: 120,
|
|
14392
14462
|
rows: 45,
|
|
14393
14463
|
cwd,
|
|
14394
|
-
env: buildProbeEnv(
|
|
14464
|
+
env: buildProbeEnv(baseEnv)
|
|
14395
14465
|
});
|
|
14396
14466
|
} catch {
|
|
14397
14467
|
resolve("");
|
|
@@ -14406,10 +14476,10 @@ var logger7 = createServiceLogger("usage-sampler");
|
|
|
14406
14476
|
var NO_SAMPLES = { samples: [], unmeasurable: null };
|
|
14407
14477
|
function isAttributable(identity, sessionToken) {
|
|
14408
14478
|
if (!identity) return { ok: true };
|
|
14409
|
-
if (identity.hasRefreshToken) {
|
|
14479
|
+
if (identity.hasRefreshToken && !identity.isConveyorOwned) {
|
|
14410
14480
|
return { ok: false, reason: "manual-login-credentials" };
|
|
14411
14481
|
}
|
|
14412
|
-
if (sessionToken && identity.accessToken && identity.accessToken !== sessionToken) {
|
|
14482
|
+
if (!(identity.hasRefreshToken && identity.isConveyorOwned) && sessionToken && identity.accessToken && identity.accessToken !== sessionToken) {
|
|
14413
14483
|
return { ok: false, reason: "credentials-token-mismatch" };
|
|
14414
14484
|
}
|
|
14415
14485
|
return { ok: true };
|
|
@@ -14522,6 +14592,9 @@ async function handlePullBranch(workDir, branch) {
|
|
|
14522
14592
|
}
|
|
14523
14593
|
}
|
|
14524
14594
|
|
|
14595
|
+
// src/runner/session-runner.ts
|
|
14596
|
+
import { rmSync } from "fs";
|
|
14597
|
+
|
|
14525
14598
|
// src/runner/background-work.ts
|
|
14526
14599
|
var BACKGROUND_WORK_MAX_MS = 45 * 60 * 1e3;
|
|
14527
14600
|
var BACKGROUND_DEFAULT_BY_TOOL = {
|
|
@@ -14632,8 +14705,8 @@ function findLiveChild(sources) {
|
|
|
14632
14705
|
|
|
14633
14706
|
// src/runner/session-runner.ts
|
|
14634
14707
|
var AUTO_RUN_MODES = /* @__PURE__ */ new Set(["building", "auto", "review", "discovery", "chat"]);
|
|
14635
|
-
var AUTONOMOUS_RUNNER_MODES = /* @__PURE__ */ new Set(["pack", "pm", "code-review"]);
|
|
14636
14708
|
var CHILD_DEFER_RECHECK_MS = 60 * 1e3;
|
|
14709
|
+
var ORPHAN_WAKE_GRACE_MS = 3 * 60 * 1e3;
|
|
14637
14710
|
var SessionRunner = class _SessionRunner {
|
|
14638
14711
|
connection;
|
|
14639
14712
|
mode;
|
|
@@ -14703,9 +14776,11 @@ var SessionRunner = class _SessionRunner {
|
|
|
14703
14776
|
onHeartbeat: () => {
|
|
14704
14777
|
const loopStatus = this.refreshLoopStatus();
|
|
14705
14778
|
this.connection.sendHeartbeat(this.loopLag.takeMaxLagMs(), loopStatus);
|
|
14779
|
+
this.maybeWakeForOrphanedBackgroundWork();
|
|
14706
14780
|
},
|
|
14707
14781
|
onIdleTimeout: () => {
|
|
14708
14782
|
if (this.deferShutdownForLiveChild("idle")) return;
|
|
14783
|
+
if (this.deferShutdownForBackgroundWork()) return;
|
|
14709
14784
|
process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
|
|
14710
14785
|
this.stopped = true;
|
|
14711
14786
|
this.queryBridge?.stop();
|
|
@@ -14786,6 +14861,133 @@ var SessionRunner = class _SessionRunner {
|
|
|
14786
14861
|
}
|
|
14787
14862
|
return true;
|
|
14788
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
|
+
}
|
|
14789
14991
|
get sessionId() {
|
|
14790
14992
|
return this.connection.sessionId;
|
|
14791
14993
|
}
|
|
@@ -15113,26 +15315,48 @@ var SessionRunner = class _SessionRunner {
|
|
|
15113
15315
|
return true;
|
|
15114
15316
|
}
|
|
15115
15317
|
/**
|
|
15116
|
-
* The mid-turn
|
|
15117
|
-
*
|
|
15118
|
-
* `--resume`" — assumes a human or a follow-up is
|
|
15119
|
-
*
|
|
15120
|
-
*
|
|
15318
|
+
* The mid-turn watchdog aborts a turn that produced no events for 30 minutes
|
|
15319
|
+
* (or one whose config-home mount died). Its documented recovery — "the next
|
|
15320
|
+
* message respawns with `--resume`" — assumes a human or a follow-up is
|
|
15321
|
+
* coming. A KICKOFF turn has neither by construction: it is the turn that
|
|
15322
|
+
* starts the conversation, so there is no next message, and the runner goes
|
|
15323
|
+
* idle with the card parked until a human notices.
|
|
15324
|
+
*
|
|
15325
|
+
* Observed live 2026-08-28 (card cmtd78iql): a spawned builder child's
|
|
15326
|
+
* kickoff turn produced nothing for 30 minutes, was aborted, and the child
|
|
15327
|
+
* then heartbeated idle for another 43 minutes until a human typed
|
|
15328
|
+
* "@[agent:builder] continue".
|
|
15329
|
+
*
|
|
15330
|
+
* So any submit-delivery INITIAL query killed by the watchdog is re-run once
|
|
15331
|
+
* against the (durable, on-disk) resumed session. Bounded to a single retry:
|
|
15332
|
+
* a second wedge is a real failure that should surface as a parked card
|
|
15333
|
+
* rather than loop the pod.
|
|
15334
|
+
*
|
|
15335
|
+
* Nothing recovers a card from there automatically, and that is the honest
|
|
15336
|
+
* state of it. The server's session sweep does NOT catch this shape: its
|
|
15337
|
+
* predicate is `agentRunningAt` still null, and a turn that started and then
|
|
15338
|
+
* wedged has already stamped it. The sweep covers a builder that never took a
|
|
15339
|
+
* turn at all; this retry covers one whose first turn stalled. Twice-wedged
|
|
15340
|
+
* needs a human, and a card left that way keeps its Builder tab and its
|
|
15341
|
+
* heartbeat, so it is visible rather than silently gone.
|
|
15342
|
+
*
|
|
15343
|
+
* Skipped when a pending message already exists, because the core loop
|
|
15344
|
+
* delivers that instead, which is the path the watchdog's original recovery
|
|
15345
|
+
* assumed.
|
|
15121
15346
|
*
|
|
15122
|
-
*
|
|
15123
|
-
*
|
|
15124
|
-
*
|
|
15125
|
-
*
|
|
15126
|
-
*
|
|
15127
|
-
*
|
|
15347
|
+
* A mount-death abort retries too, and must: the repair for a dead config
|
|
15348
|
+
* home is the respawn that follows the abort, so a kickoff with nothing
|
|
15349
|
+
* behind it would otherwise never get one.
|
|
15350
|
+
*
|
|
15351
|
+
* NOT for follow-up turns — there "the conversation resumes on the next
|
|
15352
|
+
* message" is exactly right.
|
|
15128
15353
|
*/
|
|
15129
15354
|
async requeueWedgedInitialQuery(delivery) {
|
|
15130
15355
|
if (!this.queryBridge?.lastTurnWedgeAborted) return;
|
|
15131
15356
|
if (this.stopped || this.interrupted || this.completedThisTurn) return;
|
|
15132
15357
|
if (this.pendingMessages.length > 0) return;
|
|
15133
|
-
if (!this.mode.isAuto && !AUTONOMOUS_RUNNER_MODES.has(this.config.runnerMode ?? "")) return;
|
|
15134
15358
|
process.stderr.write(
|
|
15135
|
-
"[conveyor-agent] Initial query was aborted as wedged with nothing queued behind it \u2014 re-running it once (
|
|
15359
|
+
"[conveyor-agent] Initial query was aborted as wedged with nothing queued behind it \u2014 re-running it once (a kickoff turn has no follow-up message coming)\n"
|
|
15136
15360
|
);
|
|
15137
15361
|
this.connection.sendEvent({
|
|
15138
15362
|
type: "error",
|
|
@@ -15373,7 +15597,10 @@ var SessionRunner = class _SessionRunner {
|
|
|
15373
15597
|
async sampleAndReportKeyUsage() {
|
|
15374
15598
|
if (this.stopped) return;
|
|
15375
15599
|
const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
|
|
15376
|
-
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
|
+
);
|
|
15377
15604
|
if (unmeasurable) {
|
|
15378
15605
|
this.connection.sendEvent(buildUnmeasurableEvent(unmeasurable.reason, codingAgentKeyId));
|
|
15379
15606
|
return;
|
|
@@ -15685,7 +15912,7 @@ ${outcome.failures.join("\n")}
|
|
|
15685
15912
|
*/
|
|
15686
15913
|
resolveLoopStatus() {
|
|
15687
15914
|
if (loopStatusForRunnerStatus(this._state) === "active") return "active";
|
|
15688
|
-
return this.backgroundWork.hasPending() ? "waiting" : "idle";
|
|
15915
|
+
return this.backgroundWork.hasPending() || isHeavyGateActive() ? "waiting" : "idle";
|
|
15689
15916
|
}
|
|
15690
15917
|
/** Mirror the resolved status into the loop-lag buffer so the starvation-proof
|
|
15691
15918
|
* heartbeat worker reports it too. Returns what it wrote. */
|
|
@@ -15701,6 +15928,11 @@ ${outcome.failures.join("\n")}
|
|
|
15701
15928
|
this.backgroundWork.noteToolUse(event.tool, event.input);
|
|
15702
15929
|
}
|
|
15703
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();
|
|
15704
15936
|
this._state = status;
|
|
15705
15937
|
this.refreshLoopStatus();
|
|
15706
15938
|
await this.connection.emitStatus(status);
|
|
@@ -15730,6 +15962,15 @@ ${outcome.failures.join("\n")}
|
|
|
15730
15962
|
get finalState() {
|
|
15731
15963
|
return this._finalState;
|
|
15732
15964
|
}
|
|
15965
|
+
/** Does a transcript already exist for this session's lineage? */
|
|
15966
|
+
resolvesToTranscriptResume() {
|
|
15967
|
+
const taskId = this.fullContext?.taskId;
|
|
15968
|
+
if (!taskId) return false;
|
|
15969
|
+
return hasExistingSessionFile(taskId, this.config.workspaceDir, {
|
|
15970
|
+
agentMode: this.mode.effectiveMode,
|
|
15971
|
+
runnerMode: this.config.runnerMode
|
|
15972
|
+
});
|
|
15973
|
+
}
|
|
15733
15974
|
buildTaskContextSnapshot() {
|
|
15734
15975
|
return {
|
|
15735
15976
|
isParentTask: this.fullContext?.isParentTask ?? false,
|
|
@@ -15746,6 +15987,12 @@ ${outcome.failures.join("\n")}
|
|
|
15746
15987
|
mode: this.mode.effectiveMode,
|
|
15747
15988
|
runnerMode: this.config.runnerMode ?? "task",
|
|
15748
15989
|
sessionId: this.sessionId,
|
|
15990
|
+
// Whether this run will RESUME a transcript, read from disk under this
|
|
15991
|
+
// session's own lineage. `hasExistingSession` below answers a different
|
|
15992
|
+
// question — does the server hold an sdkSessionId — and the two disagree
|
|
15993
|
+
// whenever a role's lineage is fresh. Resume is filesystem-driven, so
|
|
15994
|
+
// this is the field to read when a session's context looks wrong.
|
|
15995
|
+
resumesTranscript: this.resolvesToTranscriptResume(),
|
|
15749
15996
|
...this.buildTaskContextSnapshot(),
|
|
15750
15997
|
model: this.taskContext?.model,
|
|
15751
15998
|
isAuto: this.config.isAuto ?? false,
|