@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.
@@ -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-N4WSUTGV.js";
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(path2, onEvent, onRawRecord, mapRecord = mapTranscriptRecord) {
3138
- this.path = path2;
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
- function claudeCredentialsPath() {
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((s) => typeof s === "string" && s.length > 0) : [];
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.length > 0;
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 markerHashes = parseCredentialsMarker(input.markerRaw ?? null);
4163
+ const markerRaw = input.markerRaw ?? null;
4164
+ const markerHashes = parseCredentialsMarker(markerRaw);
4097
4165
  const contents = buildCredentialsFile(material, input.now);
4098
- const marker = buildCredentialsMarker(material.access, input.now, markerHashes[0] ?? null);
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 (!isConveyorOwnedCredentials(existing, markerHashes)) {
4175
+ if (!ownsExistingCredential(existing, markerRaw, markerHashes)) {
4102
4176
  return { action: "skip", reason: "foreign-credentials" };
4103
4177
  }
4104
- const fresh = material.refresh ? existing.accessToken === material.access : existing.accessToken === material.access && typeof existing.expiresAt === "number" && existing.expiresAt > input.now + REFRESH_SKEW_MS;
4105
- if (fresh) return { action: "skip", reason: "current" };
4106
- return { action: "write", contents, marker };
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(path2) {
4194
+ async function readRaw(path) {
4109
4195
  try {
4110
- return await readFile(path2, "utf8");
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
- const parsed = parseClaudeAiOauth(await readRaw(claudeCredentialsPath()));
4133
- if (!parsed) return null;
4134
- return {
4135
- accessToken: typeof parsed.accessToken === "string" ? parsed.accessToken : null,
4136
- hasRefreshToken: typeof parsed.refreshToken === "string" && parsed.refreshToken.length > 0
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(path2, mode) {
4237
+ function fsWriteIo(path, mode) {
4153
4238
  return {
4154
- write: (contents) => writeFile4(path2, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
4155
- read: () => readRaw(path2)
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 path2 = claudeCredentialsPath();
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(path2),
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
- const verified = await writeWithReadBackRetry(fsWriteIo(path2, 384), plan.contents);
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 ${path2} \u2014 TUI may land on the login picker
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(path2, 384).catch(() => {
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 path2 = claudeJsonPath();
4217
- const cleaned = planApprovedApiKeyCleanup(await readRaw(path2), oauthToken);
4303
+ const path = claudeJsonPath();
4304
+ const cleaned = planApprovedApiKeyCleanup(await readRaw(path), oauthToken);
4218
4305
  if (cleaned === null) return;
4219
- const verified = await writeWithReadBackRetry(fsWriteIo(path2), cleaned);
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 ${path2} \u2014 CLI may still see the poisoned entry
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 path2 = claudeJsonPath();
4359
- const existingRaw = await readRaw(path2);
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(path2), contents);
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(path2);
4461
+ const verify = await readRaw(path);
4375
4462
  process.stderr.write(
4376
- `[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path2} after retries: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
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 path2 = claudeCredentialsPath();
4476
+ const path = claudeCredentialsPath();
4390
4477
  const markerPath = conveyorCredentialsMarkerPath();
4391
- const existing = parseClaudeAiOauth(await readRaw(path2));
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(path2, { force: true });
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(path2) {
5991
+ function isExecutable(path) {
5905
5992
  try {
5906
- if (!statSync(path2).isFile()) return false;
5907
- accessSync(path2, constants.X_OK);
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(path2) {
6047
+ async function readJsonFile(path) {
5961
6048
  try {
5962
- return JSON.parse(await fs.readFile(path2, "utf8"));
6049
+ return JSON.parse(await fs.readFile(path, "utf8"));
5963
6050
  } catch {
5964
6051
  return {};
5965
6052
  }
5966
6053
  }
5967
- async function writeJsonFile(path2, value) {
5968
- await fs.mkdir(dirname2(path2), { recursive: true });
5969
- await fs.writeFile(path2, `${JSON.stringify(value, null, 2)}
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 path2 = opencodeAuthPath(env);
5974
- const store = await readJsonFile(path2);
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(path2, store);
6072
+ await writeJsonFile(path, store);
5986
6073
  logger.info("seeded opencode oauth store entry");
5987
6074
  }
5988
6075
  async function ensurePluginConfig(env) {
5989
- const path2 = opencodeConfigPath(env);
5990
- const config = await readJsonFile(path2);
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(path2, config);
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 path2 = join10(this.tempDir, "conveyor-instructions.md");
6221
- await writeFile6(path2, text, "utf8");
6222
- return path2;
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 readFileSync3, truncateSync } from "fs";
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: path2, title, tags }) => {
10550
+ return defineContractTool(uploadAttachmentContract, async ({ path, title, tags }) => {
10464
10551
  try {
10465
- const filePath = isAbsolute(path2) ? path2 : join12(config.workspaceDir, path2);
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(path2) {
13236
+ function repairTornSessionFile(path) {
13177
13237
  try {
13178
- if (!existsSync(path2)) return false;
13179
- const content = readFileSync3(path2, "utf8");
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(path2, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
13258
+ truncateSync(path, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
13199
13259
  logger5.warn("Repaired torn transcript before resume", {
13200
- path: path2,
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 cwd = deps.cwd ?? process.cwd();
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(deps.env)
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(process.env.CLAUDE_CODE_OAUTH_TOKEN);
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);