@rallycry/conveyor-agent 11.0.3 → 11.0.5

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