@rallycry/conveyor-agent 11.0.16 → 11.0.18

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startWorkbenchServer
3
- } from "./chunk-7TSQXIN3.js";
3
+ } from "./chunk-EIHZRSYI.js";
4
4
  import {
5
5
  TOMBSTONE_MESSAGE,
6
6
  isLegacyEntrypointLaunch
@@ -12,7 +12,7 @@ import {
12
12
  refreshSkillsAfterCheckout,
13
13
  runPreReadyBinds
14
14
  } from "./chunk-SR66HQKB.js";
15
- import "./chunk-QLMNQSJL.js";
15
+ import "./chunk-W5INK3NE.js";
16
16
  import {
17
17
  GitPrepJob,
18
18
  defaultGit,
@@ -20,6 +20,8 @@ import {
20
20
  reportBootMilestone
21
21
  } from "./chunk-GL2DIQEQ.js";
22
22
  import "./chunk-W4LZ7R6Z.js";
23
+ import "./chunk-372R6E4C.js";
24
+ import "./chunk-SQM2BQ7H.js";
23
25
  import {
24
26
  workbenchPort
25
27
  } from "./chunk-KMB3BU4S.js";
@@ -0,0 +1,147 @@
1
+ import {
2
+ getWorkbenchClient
3
+ } from "./chunk-SQM2BQ7H.js";
4
+ import {
5
+ sharedDir,
6
+ workbenchEnabled
7
+ } from "./chunk-KMB3BU4S.js";
8
+
9
+ // src/runner/heavy-gate.ts
10
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
11
+ import path from "path";
12
+ var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
13
+ var GATES_DIR_NAME = "gates";
14
+ function runDir() {
15
+ return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
16
+ }
17
+ function gatesDir() {
18
+ return path.join(sharedDir() ?? runDir(), GATES_DIR_NAME);
19
+ }
20
+ function pidAlive(pid) {
21
+ if (process.platform === "linux" && pidIsZombie(pid)) return false;
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ function pidIsZombie(pid) {
30
+ try {
31
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
32
+ const closingParen = stat.lastIndexOf(")");
33
+ return closingParen >= 0 && stat.slice(closingParen + 1).trimStart().startsWith("Z");
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ function parsePid(raw) {
39
+ const pid = Number.parseInt(raw.trim(), 10);
40
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
41
+ }
42
+ function singletonPidFiles() {
43
+ return GATE_KEYS.map((key) => path.join(runDir(), `${key}.pid`));
44
+ }
45
+ var splitGenericGateStatus = null;
46
+ var splitGenericGateStatusRefresh = null;
47
+ function getLocalGenericGateStatus() {
48
+ const activeLabels = [];
49
+ const abandonedReceipts = [];
50
+ let entries;
51
+ try {
52
+ entries = readdirSync(gatesDir());
53
+ } catch {
54
+ return { activeLabels, abandonedReceipts };
55
+ }
56
+ for (const entry of entries) {
57
+ if (!entry.endsWith(".pid")) continue;
58
+ const pidPath = path.join(gatesDir(), entry);
59
+ const label = entry.slice(0, -".pid".length);
60
+ try {
61
+ const pid = parsePid(readFileSync(pidPath, "utf8"));
62
+ if (pid === null) continue;
63
+ if (pidAlive(pid)) {
64
+ activeLabels.push(label);
65
+ } else if (!existsSync(path.join(gatesDir(), `${label}.exit`))) {
66
+ abandonedReceipts.push({ path: pidPath, label, pid, mtimeMs: statSync(pidPath).mtimeMs });
67
+ }
68
+ } catch {
69
+ }
70
+ }
71
+ abandonedReceipts.sort((a, b) => a.mtimeMs - b.mtimeMs);
72
+ return { activeLabels, abandonedReceipts };
73
+ }
74
+ function genericGateStatus() {
75
+ if (!workbenchEnabled()) return getLocalGenericGateStatus();
76
+ return splitGenericGateStatus ?? { activeLabels: ["unknown"], abandonedReceipts: [] };
77
+ }
78
+ function refreshGenericGateStatus() {
79
+ if (!workbenchEnabled()) return Promise.resolve(getLocalGenericGateStatus());
80
+ if (splitGenericGateStatusRefresh) return splitGenericGateStatusRefresh;
81
+ splitGenericGateStatusRefresh = getWorkbenchClient().gateStatus().then((status) => {
82
+ splitGenericGateStatus = {
83
+ activeLabels: status.activeLabels,
84
+ abandonedReceipts: status.abandonedReceipts.map((receipt) => ({
85
+ ...receipt,
86
+ path: path.join(gatesDir(), `${receipt.label}.pid`)
87
+ }))
88
+ };
89
+ return splitGenericGateStatus;
90
+ }).catch(() => {
91
+ splitGenericGateStatus = null;
92
+ return genericGateStatus();
93
+ }).finally(() => {
94
+ splitGenericGateStatusRefresh = null;
95
+ });
96
+ return splitGenericGateStatusRefresh;
97
+ }
98
+ function listAbandonedGateReceipts() {
99
+ return genericGateStatus().abandonedReceipts;
100
+ }
101
+ function listGateExitSentinels() {
102
+ const sentinels = [];
103
+ let entries;
104
+ try {
105
+ entries = readdirSync(gatesDir());
106
+ } catch {
107
+ return sentinels;
108
+ }
109
+ for (const entry of entries) {
110
+ if (!entry.endsWith(".exit")) continue;
111
+ const file = path.join(gatesDir(), entry);
112
+ try {
113
+ const mtimeMs = statSync(file).mtimeMs;
114
+ let code = null;
115
+ try {
116
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
117
+ const value = parsed?.code;
118
+ if (typeof value === "number") code = value;
119
+ } catch {
120
+ }
121
+ sentinels.push({ path: file, label: entry.slice(0, -".exit".length), code, mtimeMs });
122
+ } catch {
123
+ }
124
+ }
125
+ sentinels.sort((a, b) => a.mtimeMs - b.mtimeMs);
126
+ return sentinels;
127
+ }
128
+ function isHeavyGateActive() {
129
+ if (genericGateStatus().activeLabels.length > 0) return true;
130
+ for (const file of singletonPidFiles()) {
131
+ try {
132
+ const pid = parsePid(readFileSync(file, "utf8"));
133
+ if (pid !== null && pidAlive(pid)) return true;
134
+ } catch {
135
+ }
136
+ }
137
+ return false;
138
+ }
139
+
140
+ export {
141
+ gatesDir,
142
+ getLocalGenericGateStatus,
143
+ refreshGenericGateStatus,
144
+ listAbandonedGateReceipts,
145
+ listGateExitSentinels,
146
+ isHeavyGateActive
147
+ };
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  loadPtySpawn
3
- } from "./chunk-QLMNQSJL.js";
3
+ } from "./chunk-W5INK3NE.js";
4
4
  import {
5
5
  terminateProcessGroup
6
6
  } from "./chunk-W4LZ7R6Z.js";
7
+ import {
8
+ getLocalGenericGateStatus
9
+ } from "./chunk-372R6E4C.js";
7
10
  import {
8
11
  FrameReader,
9
12
  timingSafeTokenEqual,
@@ -112,21 +115,25 @@ async function runRefreshSkills(socket, opts) {
112
115
  }
113
116
  socket.end();
114
117
  }
118
+ function immediateResponse(req, opts) {
119
+ if (req.op === "ping") return { t: "pong", version: opts.version };
120
+ if (req.op === "gitStatus") return opts.getGitStatus?.() ?? { t: "gitStatus", state: "ready" };
121
+ if (req.op === "gateStatus") return { t: "gateStatus", ...getLocalGenericGateStatus() };
122
+ return null;
123
+ }
115
124
  function dispatch(socket, req, sink, opts, liveOps) {
116
125
  if (!req || typeof req !== "object" || !timingSafeTokenEqual(req.token ?? "", opts.token)) {
117
126
  writeFrame(socket, { t: "error", message: "unauthorized", code: "unauthorized" });
118
127
  socket.destroy();
119
128
  return;
120
129
  }
130
+ const immediate = immediateResponse(req, opts);
131
+ if (immediate) {
132
+ writeFrame(socket, immediate);
133
+ socket.end();
134
+ return;
135
+ }
121
136
  switch (req.op) {
122
- case "ping":
123
- writeFrame(socket, { t: "pong", version: opts.version });
124
- socket.end();
125
- return;
126
- case "gitStatus":
127
- writeFrame(socket, opts.getGitStatus?.() ?? { t: "gitStatus", state: "ready" });
128
- socket.end();
129
- return;
130
137
  case "refreshSkills":
131
138
  void runRefreshSkills(socket, opts);
132
139
  return;
@@ -5,7 +5,7 @@ import {
5
5
  readWorkspaceBytes,
6
6
  statWorkspacePath,
7
7
  workspacePathExists
8
- } from "./chunk-M4SD2ESQ.js";
8
+ } from "./chunk-WS7QRB37.js";
9
9
  import {
10
10
  reportBootMilestone
11
11
  } from "./chunk-GL2DIQEQ.js";
@@ -16,7 +16,7 @@ import {
16
16
  } from "./chunk-W4LZ7R6Z.js";
17
17
  import {
18
18
  getWorkbenchClient
19
- } from "./chunk-B54XFOXL.js";
19
+ } from "./chunk-SQM2BQ7H.js";
20
20
  import {
21
21
  workbenchEnabled
22
22
  } from "./chunk-KMB3BU4S.js";
@@ -37,7 +37,31 @@ var RemoteProcessHandle = class extends EventEmitter {
37
37
  }
38
38
  };
39
39
 
40
+ // src/workbench/gate-status.ts
41
+ function readGateStatus(open, timeoutMs) {
42
+ return new Promise((resolve, reject) => {
43
+ let status = null;
44
+ let timer = null;
45
+ const socket = open(
46
+ (frame) => {
47
+ if (frame.t === "gateStatus") status = frame;
48
+ },
49
+ (error) => {
50
+ if (timer) clearTimeout(timer);
51
+ if (status) resolve(status);
52
+ else reject(error ?? new WorkbenchError("connection closed before gateStatus"));
53
+ }
54
+ );
55
+ timer = setTimeout(
56
+ () => socket.destroy(new WorkbenchError("gateStatus timed out", "workbench_timeout")),
57
+ timeoutMs
58
+ );
59
+ timer.unref();
60
+ });
61
+ }
62
+
40
63
  // src/workbench/client.ts
64
+ var DEFAULT_GATE_STATUS_TIMEOUT_MS = 5e3;
41
65
  var WORKBENCH_COMMAND_ENV_DENYLIST = /* @__PURE__ */ new Set([
42
66
  "POD_BOOTSTRAP_TOKEN",
43
67
  "CONVEYOR_BOOTSTRAP_TOKEN",
@@ -107,10 +131,12 @@ var WorkbenchClient = class {
107
131
  port;
108
132
  host;
109
133
  token;
134
+ gateStatusTimeoutMs;
110
135
  constructor(options = {}) {
111
136
  this.port = options.port ?? workbenchPort() ?? DEFAULT_WORKBENCH_PORT;
112
137
  this.host = options.host ?? "127.0.0.1";
113
138
  this.token = options.token ?? workbenchToken();
139
+ this.gateStatusTimeoutMs = options.gateStatusTimeoutMs ?? DEFAULT_GATE_STATUS_TIMEOUT_MS;
114
140
  }
115
141
  /** Open a connection, send the request, route response frames. */
116
142
  open(request, onFrame, onClose) {
@@ -168,6 +194,12 @@ var WorkbenchClient = class {
168
194
  );
169
195
  });
170
196
  }
197
+ gateStatus() {
198
+ return readGateStatus(
199
+ (onFrame, onClose) => this.open({ op: "gateStatus" }, onFrame, onClose),
200
+ this.gateStatusTimeoutMs
201
+ );
202
+ }
171
203
  /** Ask the workbench — which owns the repo filesystem in split mode — to
172
204
  * re-run the skill floor after the agent's authoritative task-branch
173
205
  * checkout. A daemon without a provider answers an empty result. */
@@ -200,7 +200,7 @@ async function loadPtySpawn() {
200
200
  async function resolvePtySpawn() {
201
201
  const { workbenchEnabled } = await import("./mode-ZJSOSLGU.js");
202
202
  if (!workbenchEnabled()) return loadPtySpawn();
203
- const { getWorkbenchClient } = await import("./client-DVVHMIOO.js");
203
+ const { getWorkbenchClient } = await import("./client-VIFYMBT2.js");
204
204
  return (file, args, options) => getWorkbenchClient().spawnPty(file, args, options);
205
205
  }
206
206
  function sessionTempBase() {
@@ -20,7 +20,7 @@ import {
20
20
  import {
21
21
  WorkbenchError,
22
22
  getWorkbenchClient
23
- } from "./chunk-B54XFOXL.js";
23
+ } from "./chunk-SQM2BQ7H.js";
24
24
  import {
25
25
  workbenchEnabled
26
26
  } from "./chunk-KMB3BU4S.js";
@@ -29,7 +29,7 @@ import {
29
29
  spawnOptionsFingerprint,
30
30
  transcriptSize,
31
31
  turnOptionsFrom
32
- } from "./chunk-QLMNQSJL.js";
32
+ } from "./chunk-W5INK3NE.js";
33
33
  import {
34
34
  AgentConnection,
35
35
  CodespacePortVisibility,
@@ -59,7 +59,7 @@ import {
59
59
  statWorkspacePath,
60
60
  updateRemoteToken,
61
61
  verifyGitCredential
62
- } from "./chunk-M4SD2ESQ.js";
62
+ } from "./chunk-WS7QRB37.js";
63
63
  import {
64
64
  registerBootMilestoneSocketFallback,
65
65
  reportBootMilestone
@@ -72,15 +72,17 @@ import {
72
72
  } from "./chunk-W4LZ7R6Z.js";
73
73
  import {
74
74
  isHeavyGateActive,
75
- listGateExitSentinels
76
- } from "./chunk-7P6QZHXZ.js";
75
+ listAbandonedGateReceipts,
76
+ listGateExitSentinels,
77
+ refreshGenericGateStatus
78
+ } from "./chunk-372R6E4C.js";
77
79
  import {
78
80
  LoopLagMonitor,
79
81
  loopStatusForRunnerStatus
80
82
  } from "./chunk-IA45XHOA.js";
81
83
  import {
82
84
  getWorkbenchClient
83
- } from "./chunk-B54XFOXL.js";
85
+ } from "./chunk-SQM2BQ7H.js";
84
86
  import {
85
87
  workbenchEnabled
86
88
  } from "./chunk-KMB3BU4S.js";
@@ -2181,6 +2183,10 @@ var CATALOG = {
2181
2183
  // rejects (media_type_header_exception), breaking search/audit indexing
2182
2184
  // in pods. 512m heap matches the project compose sizing.
2183
2185
  image: "docker.elastic.co/elasticsearch/elasticsearch:9.4.0",
2186
+ mirror: {
2187
+ src: "docker.elastic.co/elasticsearch/elasticsearch:9.4.0",
2188
+ dest: "mirror-elasticsearch:9.4.0"
2189
+ },
2184
2190
  ports: [9200],
2185
2191
  // Baked service images are `docker commit`s of a recently-running ES, so
2186
2192
  // they carry a stale data-dir node.lock; ES 9 hard-fails on it at boot
@@ -8517,7 +8523,7 @@ Workflow:`,
8517
8523
  `- If you toggled into active mode temporarily, mention when you're done so the team can switch you back to planning mode.`
8518
8524
  ].filter(Boolean);
8519
8525
  }
8520
- function buildTaskAgentPreamble(context, workspaceDir) {
8526
+ function buildTaskAgentPreamble(context, workspaceDir, runtimeTui) {
8521
8527
  const managedStack = repoHasScript(workspaceDir, "web:rebuild");
8522
8528
  const stackLines = managedStack ? [
8523
8529
  `- The web app is served on port 3050, the API on port 7090.`,
@@ -8543,8 +8549,8 @@ Working rules:`,
8543
8549
  `- Read a file before your first Write/Edit to it, and batch multiple changes to the same file into a single call instead of many sequential edits.`,
8544
8550
  `- To learn what calls a symbol or where it lives, query the prebuilt code graph before grepping: \`graphify query "<SymbolName>"\` from the repo root. Query a SYMBOL, never a sentence \u2014 \`graphify query "resolveTaskBaseBranch"\` returns the definition plus every call site, while "how does a task get its base branch" seeds unrelated start nodes and returns test files and loggers. Don't know the symbol yet? Grep for the name first, then query it: grep finds names, the graph finds relationships. \`No matching nodes found\` means "not in this graph" (it is prebuilt, so very recent code is absent), NOT "not in the codebase" \u2014 fall back to \`git grep\`. Skip all of this if \`graphify-out/graph.json\` is not present.`,
8545
8551
  `- When a build/lint/test run fails, capture its output to a file once and grep the file \u2014 never re-run the suite just to re-filter the same output.`,
8546
- `- Waiting on long-running commands: if a gate finishes in under ~2 minutes, run it in the foreground with a timeout. For a longer one, launch it with run_in_background and STOP; a completion notification arrives when it finishes, and the workspace stays awake for as long as background work is outstanding, so a backgrounded gate will not be killed by an idle sleep. For the final pre-PR gate a bounded foreground run (\`timeout 590 <gate>\` with Bash \`timeout: 600000\`) is still preferred as defense in depth \u2014 it survives a pod resume, which a background job does not. Never busy-wait with sleep/pgrep/tail loops, and never re-run the suite to escape a wait that looks stalled.`,
8547
- `- Ending your turn with NO tool call is the correct way to wait, and it is safe: the pod stays alive and the next notification re-invokes you. Never emit filler commands (\`echo waiting\`, \`true\`, \`sleep N; echo done\`) to "stay alive" \u2014 they are detected and blocked. The proven long-wait shape: start the job with run_in_background, then end the turn. Arm a ScheduleWakeup (delaySeconds 900-1500, prompt restating your next steps) only when nothing will notify you \u2014 an external CI run, a deploy, a remote queue \u2014 never as insurance against a background job's own notification, which does fire.`,
8552
+ runtimeTui === "codex" ? `- Waiting on long-running commands: retain and resume the command session until it returns an exit result. Finish each required gate before ending the task. Never pretend a completion notification will resume you, and never start another gate while the current one is running.` : `- Waiting on long-running commands: if a gate finishes in under ~2 minutes, run it in the foreground with a timeout. For a longer one, launch it with run_in_background and STOP; a completion notification arrives when it finishes, and the workspace stays awake for as long as background work is outstanding, so a backgrounded gate will not be killed by an idle sleep. For the final pre-PR gate a bounded foreground run (\`timeout 590 <gate>\` with Bash \`timeout: 600000\`) is still preferred as defense in depth \u2014 it survives a pod resume, which a background job does not. Never busy-wait with sleep/pgrep/tail loops, and never re-run the suite to escape a wait that looks stalled.`,
8553
+ runtimeTui === "codex" ? `- Do not end your turn to wait on a required gate. Resume its command session, inspect its exit result, and then continue the checklist.` : `- Ending your turn with NO tool call is the correct way to wait, and it is safe: the pod stays alive and the next notification re-invokes you. Never emit filler commands (\`echo waiting\`, \`true\`, \`sleep N; echo done\`) to "stay alive" \u2014 they are detected and blocked. The proven long-wait shape: start the job with run_in_background, then end the turn. Arm a ScheduleWakeup (delaySeconds 900-1500, prompt restating your next steps) only when nothing will notify you \u2014 an external CI run, a deploy, a remote queue \u2014 never as insurance against a background job's own notification, which does fire.`,
8548
8554
  `
8549
8555
  Git:`,
8550
8556
  `- Stay on \`${context.githubBranch}\` for the whole task: do not check out another branch and do not create one. It was cut from \`${context.baseBranch}\`, and PRs target that automatically.`,
@@ -8561,7 +8567,7 @@ function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
8561
8567
  if (isPackRunner) {
8562
8568
  return buildPackPrompt(mode, context, config, setupLog);
8563
8569
  }
8564
- const parts = isPmActive ? buildActivePreamble(context, config.workspaceDir) : isPm ? buildPmPreamble(context) : buildTaskAgentPreamble(context, config.workspaceDir);
8570
+ const parts = isPmActive ? buildActivePreamble(context, config.workspaceDir) : isPm ? buildPmPreamble(context) : buildTaskAgentPreamble(context, config.workspaceDir, config.runtimeTui);
8565
8571
  if (setupLog.length > 0) {
8566
8572
  parts.push(
8567
8573
  `
@@ -13656,10 +13662,14 @@ var ToolLoopTracker = class {
13656
13662
  };
13657
13663
  function buildRepeatLoopMessage(repeatCount, heavyGateActive) {
13658
13664
  const head = `Conveyor blocked this call: you have run the exact same command ${repeatCount} times in a row with nothing different in between. Repeating it again will return the same result.`;
13659
- const advice = heavyGateActive ? `A build gate (test/typecheck/build) is running on this pod right now. Do NOT poll its log. End your turn \u2014 the completion notification re-invokes you when the gate finishes.` : `If you are waiting on a background job, end your turn instead of polling; the completion notification re-invokes you. If you are stuck, change your approach: read a different file, run a different command, or post to chat and ask the team.`;
13665
+ const codex = process.env.CONVEYOR_TUI === "codex";
13666
+ const advice = codex ? heavyGateActive ? `A build gate is running. Resume its command session for the exit result, then continue the required gates.` : `If a command is still running, resume its session for the result. Otherwise change your approach or post to chat.` : heavyGateActive ? `A build gate (test/typecheck/build) is running on this pod right now. Do NOT poll its log. End your turn \u2014 the completion notification re-invokes you when the gate finishes.` : `If you are waiting on a background job, end your turn instead of polling; the completion notification re-invokes you. If you are stuck, change your approach: read a different file, run a different command, or post to chat and ask the team.`;
13660
13667
  return `${head} ${advice} Call a different tool now.`;
13661
13668
  }
13662
13669
  function buildNoOpKeepAliveMessage(noOpCount) {
13670
+ if (process.env.CONVEYOR_TUI === "codex") {
13671
+ return `Conveyor blocked this call: it is a no-op keep-alive (${noOpCount} this session). If a required command is running, resume its session for the exit result. Otherwise do real work or end the turn.`;
13672
+ }
13663
13673
  return `Conveyor blocked this call: it is a no-op keep-alive (${noOpCount} this session). You do not need to emit tool calls to stay alive. Ending your turn with NO tool call is safe and expected while waiting: the pod stays up and the completion notification re-invokes you. If nothing is running, do real work or end the turn. For a long wait with no notification source, use Monitor or ScheduleWakeup instead of filler commands.`;
13664
13674
  }
13665
13675
  function buildRepeatLoopChatMessage(repeatCount, forceStopped) {
@@ -14110,7 +14120,7 @@ function buildQueryOptions(host, context) {
14110
14120
  const systemPromptText = buildSystemPrompt(
14111
14121
  host.config.mode,
14112
14122
  context,
14113
- { ...host.config, isAuto: host.isAuto },
14123
+ { ...host.config, isAuto: host.isAuto, runtimeTui: process.env.CONVEYOR_TUI },
14114
14124
  host.setupLog,
14115
14125
  mode
14116
14126
  );
@@ -15684,7 +15694,7 @@ var SessionRunner = class _SessionRunner {
15684
15694
  onHeartbeat: () => {
15685
15695
  const loopStatus = this.refreshLoopStatus();
15686
15696
  this.connection.sendHeartbeat(this.loopLag.takeMaxLagMs(), loopStatus);
15687
- this.maybeWakeForOrphanedBackgroundWork();
15697
+ void this.refreshGateStatusAndMaybeWake();
15688
15698
  },
15689
15699
  onIdleTimeout: () => {
15690
15700
  if (this.deferShutdownForLiveChild("idle")) return;
@@ -15825,6 +15835,10 @@ var SessionRunner = class _SessionRunner {
15825
15835
  /** One-shot latch: at most one watchdog wake per idle episode. Reset when a
15826
15836
  * turn runs. */
15827
15837
  backgroundWakeFiredThisIdle = false;
15838
+ /** Start of the last active episode. A gate can be launched shortly before
15839
+ * the agent declares completion, so idle-start freshness would reject its
15840
+ * terminal receipt as stale. */
15841
+ activeEpisodeStartedAt = Date.now();
15828
15842
  /** When the current idle episode began — an exit sentinel older than this
15829
15843
  * appeared while a turn could still have consumed it, so it never wakes. */
15830
15844
  idleEpisodeStartedAt = 0;
@@ -15839,22 +15853,30 @@ var SessionRunner = class _SessionRunner {
15839
15853
  * evidence of orphaned background work — a tracked launch with no live gate
15840
15854
  * process continuously past `ORPHAN_WAKE_GRACE_MS`, or an unconsumed
15841
15855
  * `gates/<label>.exit` sentinel that appeared this idle episode — never on
15842
- * "the agent seems stuck". A `completed` agent is never woken (the
15843
- * completion guard holds), and a live pid always suppresses the wake.
15856
+ * "the agent seems stuck". A `completed` agent remains parked unless a
15857
+ * current-episode generic receipt proves a required gate died without an
15858
+ * exit record, and a live pid always suppresses the wake.
15844
15859
  */
15845
15860
  maybeWakeForOrphanedBackgroundWork() {
15846
15861
  try {
15847
- if (this._state !== "idle" || this.stopped || this.completedThisTurn) {
15862
+ if (this._state !== "idle" || this.stopped) {
15848
15863
  this.orphanGraceStartedAt = null;
15849
15864
  return;
15850
15865
  }
15851
15866
  if (this.backgroundWakeFiredThisIdle) return;
15852
15867
  if (!this.inputResolver) return;
15868
+ const abandoned = listAbandonedGateReceipts().find(
15869
+ (receipt) => receipt.mtimeMs >= this.activeEpisodeStartedAt
15870
+ );
15871
+ if (this.completedThisTurn && !abandoned) {
15872
+ this.orphanGraceStartedAt = null;
15873
+ return;
15874
+ }
15853
15875
  if (isHeavyGateActive()) {
15854
15876
  this.orphanGraceStartedAt = null;
15855
15877
  return;
15856
15878
  }
15857
- const evidence = this.findOrphanEvidence(Date.now());
15879
+ const evidence = abandoned ? this.abandonedReceiptEvidence(abandoned) : this.findOrphanEvidence(Date.now());
15858
15880
  if (!evidence) return;
15859
15881
  process.stderr.write(
15860
15882
  `[conveyor-agent] Background-work watchdog: waking agent \u2014 ${evidence.description}
@@ -15863,11 +15885,16 @@ var SessionRunner = class _SessionRunner {
15863
15885
  this.backgroundWakeFiredThisIdle = true;
15864
15886
  this.orphanGraceStartedAt = null;
15865
15887
  if (evidence.sentinel) rmSync(evidence.sentinel.path, { force: true });
15888
+ if (evidence.abandonedReceipt) rmSync(evidence.abandonedReceipt.path, { force: true });
15866
15889
  this.backgroundWork.clear();
15890
+ if (evidence.abandonedReceipt) {
15891
+ this.completedThisTurn = false;
15892
+ this.dormantDeadline = null;
15893
+ }
15867
15894
  const resolver = this.inputResolver;
15868
15895
  this.inputResolver = null;
15869
15896
  resolver({
15870
- 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.`,
15897
+ content: `A required gate 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_SHARED_DIR/gates on split pods \u2014 to determine the real result, then continue your plan. Do not assume the job succeeded; verify its output first. If it died, rerun it in the foreground without shell '&' and retain the command session until its exit result.`,
15871
15898
  userId: "system",
15872
15899
  source: "background_work_check"
15873
15900
  });
@@ -15898,6 +15925,25 @@ var SessionRunner = class _SessionRunner {
15898
15925
  description: `${this.backgroundWork.pendingCount()} tracked background launch(es) with no live gate process for ${Math.round(ORPHAN_WAKE_GRACE_MS / 6e4)}+ minutes`
15899
15926
  };
15900
15927
  }
15928
+ abandonedReceiptEvidence(receipt) {
15929
+ return {
15930
+ abandonedReceipt: receipt,
15931
+ description: `gate '${receipt.label}' stopped before writing an exit receipt (pid ${receipt.pid})`
15932
+ };
15933
+ }
15934
+ /** Refresh PID liveness in the owning workbench namespace before checking
15935
+ * terminal receipts. The local path is immediate; socket failures preserve
15936
+ * the conservative cached state and never interrupt the heartbeat. */
15937
+ async refreshGateStatusAndMaybeWake() {
15938
+ try {
15939
+ await refreshGenericGateStatus();
15940
+ this.refreshLoopStatus();
15941
+ this.maybeWakeForOrphanedBackgroundWork();
15942
+ } catch (err) {
15943
+ process.stderr.write(`[conveyor-agent] Gate status refresh failed: ${err}
15944
+ `);
15945
+ }
15946
+ }
15901
15947
  get sessionId() {
15902
15948
  return this.connection.sessionId;
15903
15949
  }
@@ -16709,9 +16755,12 @@ var SessionRunner = class _SessionRunner {
16709
16755
  };
16710
16756
  const bridge = new QueryBridge(this.connection, this.mode, runnerConfig, {
16711
16757
  onStatusChange: (status) => {
16712
- if (status === "running" && this._state === "waiting_for_input") {
16713
- this._state = "running";
16714
- this.lifecycle.cancelIdleTimer();
16758
+ if (status === "running") {
16759
+ this.beginRunningEpisode();
16760
+ if (this._state === "waiting_for_input") {
16761
+ this._state = "running";
16762
+ this.lifecycle.cancelIdleTimer();
16763
+ }
16715
16764
  }
16716
16765
  return this.callbacks.onStatusChange(status);
16717
16766
  },
@@ -16959,8 +17008,7 @@ ${outcome.failures.join("\n")}
16959
17008
  }
16960
17009
  async setState(status) {
16961
17010
  if (status === "running") {
16962
- this.backgroundDeferStartedAt = null;
16963
- this.backgroundWakeFiredThisIdle = false;
17011
+ this.beginRunningEpisode();
16964
17012
  }
16965
17013
  if (status === "idle" && this._state !== "idle") this.idleEpisodeStartedAt = Date.now();
16966
17014
  this._state = status;
@@ -16968,6 +17016,13 @@ ${outcome.failures.join("\n")}
16968
17016
  await this.connection.emitStatus(status);
16969
17017
  await this.callbacks.onStatusChange(status);
16970
17018
  }
17019
+ /** Mark a fresh turn without emitting a duplicate status. QueryExecutor's
17020
+ * prefilled PTY callback already emitted `running` before it reaches here. */
17021
+ beginRunningEpisode() {
17022
+ this.activeEpisodeStartedAt = Date.now();
17023
+ this.backgroundDeferStartedAt = null;
17024
+ this.backgroundWakeFiredThisIdle = false;
17025
+ }
16971
17026
  async shutdown(finalState) {
16972
17027
  process.stderr.write(`[conveyor-agent] Shutdown: reason=${finalState}
16973
17028
  `);
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  WorkspaceCommandSupervisor,
8
8
  startWorkspaceCommandsAfterConnect,
9
9
  stopWorkspaceCommands
10
- } from "./chunk-X2MKHJBZ.js";
10
+ } from "./chunk-GZRZGBIK.js";
11
11
  import {
12
12
  DEFAULT_SONNET_MODEL,
13
13
  PtyHarness,
@@ -27,13 +27,13 @@ import {
27
27
  resolveTuiKindFromEnv,
28
28
  runUsageProbe,
29
29
  sampleKeyUsage
30
- } from "./chunk-GAUVITY7.js";
30
+ } from "./chunk-ZFHN7GO7.js";
31
31
  import "./chunk-SR66HQKB.js";
32
32
  import {
33
33
  inheritedEnv,
34
34
  resolvePtySpawn,
35
35
  sessionTempBase
36
- } from "./chunk-QLMNQSJL.js";
36
+ } from "./chunk-W5INK3NE.js";
37
37
  import {
38
38
  AgentConnection,
39
39
  CodespacePortVisibility,
@@ -44,12 +44,12 @@ import {
44
44
  createServiceLogger,
45
45
  fetchBootstrap,
46
46
  loadConveyorConfig
47
- } from "./chunk-M4SD2ESQ.js";
47
+ } from "./chunk-WS7QRB37.js";
48
48
  import "./chunk-GL2DIQEQ.js";
49
49
  import "./chunk-W4LZ7R6Z.js";
50
- import "./chunk-7P6QZHXZ.js";
50
+ import "./chunk-372R6E4C.js";
51
51
  import "./chunk-IA45XHOA.js";
52
- import "./chunk-B54XFOXL.js";
52
+ import "./chunk-SQM2BQ7H.js";
53
53
  import "./chunk-KMB3BU4S.js";
54
54
  import "./chunk-6Q6LQBWO.js";
55
55
 
@@ -1205,7 +1205,7 @@ function wireSpawnChildren(mode, connection, supervisors, logger7) {
1205
1205
 
1206
1206
  // src/cli.ts
1207
1207
  if (process.argv[2] === "boot") {
1208
- const { runBoot } = await import("./boot-CCRZMY2P.js");
1208
+ const { runBoot } = await import("./boot-4ZNOCKFZ.js");
1209
1209
  process.exit(await runBoot(process.argv.slice(3)));
1210
1210
  }
1211
1211
  if (isLegacyEntrypointLaunch(process.env)) {
@@ -1307,7 +1307,7 @@ process.on("unhandledRejection", (reason) => {
1307
1307
  process.exit(1);
1308
1308
  });
1309
1309
  if (process.env.CONVEYOR_MODE === "workbench") {
1310
- const { startWorkbenchServer } = await import("./server-WM23BC5C.js");
1310
+ const { startWorkbenchServer } = await import("./server-L5CQ5EJB.js");
1311
1311
  const { oomWatchdogOptionsFromEnv } = await import("./oom-watchdog-PAC5OJJG.js");
1312
1312
  const { DEFAULT_WORKBENCH_PORT } = await import("./protocol-QBCYO4GI.js");
1313
1313
  const port = Number(process.env.CONVEYOR_WORKBENCH_PORT) || DEFAULT_WORKBENCH_PORT;
@@ -1458,7 +1458,7 @@ if (!RUNNER_MODES.includes(CONVEYOR_MODE)) {
1458
1458
  process.exit(1);
1459
1459
  }
1460
1460
  if (CONVEYOR_MODE === "serving") {
1461
- const { runServingSession } = await import("./serve-boot-VJR6J5PL.js");
1461
+ const { runServingSession } = await import("./serve-boot-RU5BYIK4.js");
1462
1462
  exitContext.runnerMode = "serving";
1463
1463
  exitContext.sessionId = process.env.CONVEYOR_SESSION_ID ?? exitContext.sessionId;
1464
1464
  const outcome = await runServingSession({
@@ -4,7 +4,7 @@ import {
4
4
  WorkbenchError,
5
5
  getWorkbenchClient,
6
6
  resetWorkbenchClient
7
- } from "./chunk-B54XFOXL.js";
7
+ } from "./chunk-SQM2BQ7H.js";
8
8
  import "./chunk-KMB3BU4S.js";
9
9
  import "./chunk-6Q6LQBWO.js";
10
10
  export {
package/dist/gate-cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  gatesDir
4
- } from "./chunk-7P6QZHXZ.js";
4
+ } from "./chunk-372R6E4C.js";
5
+ import "./chunk-SQM2BQ7H.js";
6
+ import "./chunk-KMB3BU4S.js";
7
+ import "./chunk-6Q6LQBWO.js";
5
8
 
6
9
  // src/runner/gate-wrapper.ts
7
10
  import { mkdirSync, rmSync, writeFileSync } from "fs";
package/dist/index.d.ts CHANGED
@@ -810,6 +810,10 @@ declare class SessionRunner {
810
810
  /** One-shot latch: at most one watchdog wake per idle episode. Reset when a
811
811
  * turn runs. */
812
812
  private backgroundWakeFiredThisIdle;
813
+ /** Start of the last active episode. A gate can be launched shortly before
814
+ * the agent declares completion, so idle-start freshness would reject its
815
+ * terminal receipt as stale. */
816
+ private activeEpisodeStartedAt;
813
817
  /** When the current idle episode began — an exit sentinel older than this
814
818
  * appeared while a turn could still have consumed it, so it never wakes. */
815
819
  private idleEpisodeStartedAt;
@@ -824,13 +828,19 @@ declare class SessionRunner {
824
828
  * evidence of orphaned background work — a tracked launch with no live gate
825
829
  * process continuously past `ORPHAN_WAKE_GRACE_MS`, or an unconsumed
826
830
  * `gates/<label>.exit` sentinel that appeared this idle episode — never on
827
- * "the agent seems stuck". A `completed` agent is never woken (the
828
- * completion guard holds), and a live pid always suppresses the wake.
831
+ * "the agent seems stuck". A `completed` agent remains parked unless a
832
+ * current-episode generic receipt proves a required gate died without an
833
+ * exit record, and a live pid always suppresses the wake.
829
834
  */
830
835
  private maybeWakeForOrphanedBackgroundWork;
831
836
  /** Orphan evidence that has held past the grace, or null. Maintains the
832
837
  * grace clock for the tracked-work condition as a side effect. */
833
838
  private findOrphanEvidence;
839
+ private abandonedReceiptEvidence;
840
+ /** Refresh PID liveness in the owning workbench namespace before checking
841
+ * terminal receipts. The local path is immediate; socket failures preserve
842
+ * the conservative cached state and never interrupt the heartbeat. */
843
+ private refreshGateStatusAndMaybeWake;
834
844
  get sessionId(): string;
835
845
  get isStopped(): boolean;
836
846
  /** Wire the boot supervisor handle post-construction — cli.ts constructs it
@@ -1097,6 +1107,9 @@ declare class SessionRunner {
1097
1107
  * the tool starts work that outlives the turn. */
1098
1108
  private noteToolUseForBackgroundWork;
1099
1109
  private setState;
1110
+ /** Mark a fresh turn without emitting a duplicate status. QueryExecutor's
1111
+ * prefilled PTY callback already emitted `running` before it reaches here. */
1112
+ private beginRunningEpisode;
1100
1113
  private shutdown;
1101
1114
  private _finalState;
1102
1115
  /** The final status after run() completes. Use to determine exit code. */
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  SessionRunner,
3
3
  unshallowRepo
4
- } from "./chunk-GAUVITY7.js";
4
+ } from "./chunk-ZFHN7GO7.js";
5
5
  import "./chunk-SR66HQKB.js";
6
- import "./chunk-QLMNQSJL.js";
6
+ import "./chunk-W5INK3NE.js";
7
7
  import {
8
8
  AgentConnection,
9
9
  GIT_TIMEOUT_MS,
@@ -17,18 +17,18 @@ import {
17
17
  stageAndCommit,
18
18
  updateRemoteToken,
19
19
  workspacePathExists
20
- } from "./chunk-M4SD2ESQ.js";
20
+ } from "./chunk-WS7QRB37.js";
21
21
  import "./chunk-GL2DIQEQ.js";
22
22
  import {
23
23
  runAuthTokenCommand,
24
24
  runSetupCommand,
25
25
  runStartCommand
26
26
  } from "./chunk-W4LZ7R6Z.js";
27
- import "./chunk-7P6QZHXZ.js";
27
+ import "./chunk-372R6E4C.js";
28
28
  import "./chunk-IA45XHOA.js";
29
29
  import {
30
30
  getWorkbenchClient
31
- } from "./chunk-B54XFOXL.js";
31
+ } from "./chunk-SQM2BQ7H.js";
32
32
  import {
33
33
  workbenchEnabled
34
34
  } from "./chunk-KMB3BU4S.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WorkspaceCommandSupervisor
3
- } from "./chunk-X2MKHJBZ.js";
3
+ } from "./chunk-GZRZGBIK.js";
4
4
  import {
5
5
  AgentConnection,
6
6
  CodespacePortVisibility,
@@ -11,11 +11,11 @@ import {
11
11
  createServiceLogger,
12
12
  ensureOnTaskBranch,
13
13
  loadConveyorConfig
14
- } from "./chunk-M4SD2ESQ.js";
14
+ } from "./chunk-WS7QRB37.js";
15
15
  import "./chunk-GL2DIQEQ.js";
16
16
  import "./chunk-W4LZ7R6Z.js";
17
17
  import "./chunk-IA45XHOA.js";
18
- import "./chunk-B54XFOXL.js";
18
+ import "./chunk-SQM2BQ7H.js";
19
19
  import "./chunk-KMB3BU4S.js";
20
20
  import "./chunk-6Q6LQBWO.js";
21
21
 
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  startWorkbenchServer
3
- } from "./chunk-7TSQXIN3.js";
4
- import "./chunk-QLMNQSJL.js";
3
+ } from "./chunk-EIHZRSYI.js";
4
+ import "./chunk-W5INK3NE.js";
5
5
  import "./chunk-W4LZ7R6Z.js";
6
+ import "./chunk-372R6E4C.js";
7
+ import "./chunk-SQM2BQ7H.js";
8
+ import "./chunk-KMB3BU4S.js";
6
9
  import "./chunk-6Q6LQBWO.js";
7
10
  import "./chunk-6W6UZ4SJ.js";
8
11
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "11.0.16",
3
+ "version": "11.0.18",
4
4
  "description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
5
5
  "keywords": [
6
6
  "agent",
@@ -1,76 +0,0 @@
1
- // src/runner/heavy-gate.ts
2
- import { readdirSync, readFileSync, statSync } from "fs";
3
- import path from "path";
4
- var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
5
- var GATES_DIR_NAME = "gates";
6
- function runDir() {
7
- return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
8
- }
9
- function gatesDir() {
10
- return path.join(runDir(), GATES_DIR_NAME);
11
- }
12
- function pidAlive(pid) {
13
- try {
14
- process.kill(pid, 0);
15
- return true;
16
- } catch {
17
- return false;
18
- }
19
- }
20
- function parsePid(raw) {
21
- const pid = Number.parseInt(raw.trim(), 10);
22
- return Number.isInteger(pid) && pid > 0 ? pid : null;
23
- }
24
- function gatePidFiles() {
25
- const files = GATE_KEYS.map((key) => path.join(runDir(), `${key}.pid`));
26
- try {
27
- for (const entry of readdirSync(gatesDir())) {
28
- if (entry.endsWith(".pid")) files.push(path.join(gatesDir(), entry));
29
- }
30
- } catch {
31
- }
32
- return files;
33
- }
34
- function listGateExitSentinels() {
35
- const sentinels = [];
36
- let entries;
37
- try {
38
- entries = readdirSync(gatesDir());
39
- } catch {
40
- return sentinels;
41
- }
42
- for (const entry of entries) {
43
- if (!entry.endsWith(".exit")) continue;
44
- const file = path.join(gatesDir(), entry);
45
- try {
46
- const mtimeMs = statSync(file).mtimeMs;
47
- let code = null;
48
- try {
49
- const parsed = JSON.parse(readFileSync(file, "utf8"));
50
- const value = parsed?.code;
51
- if (typeof value === "number") code = value;
52
- } catch {
53
- }
54
- sentinels.push({ path: file, label: entry.slice(0, -".exit".length), code, mtimeMs });
55
- } catch {
56
- }
57
- }
58
- sentinels.sort((a, b) => a.mtimeMs - b.mtimeMs);
59
- return sentinels;
60
- }
61
- function isHeavyGateActive() {
62
- for (const file of gatePidFiles()) {
63
- try {
64
- const pid = parsePid(readFileSync(file, "utf8"));
65
- if (pid !== null && pidAlive(pid)) return true;
66
- } catch {
67
- }
68
- }
69
- return false;
70
- }
71
-
72
- export {
73
- gatesDir,
74
- listGateExitSentinels,
75
- isHeavyGateActive
76
- };