@rallycry/conveyor-agent 7.3.4 → 7.3.6

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.
@@ -641,8 +641,10 @@ var ModeController = class {
641
641
  // src/runner/lifecycle.ts
642
642
  var DEFAULT_LIFECYCLE_CONFIG = {
643
643
  idleTimeoutMs: 30 * 60 * 1e3,
644
+ dormantTimeoutMs: 60 * 60 * 1e3,
644
645
  heartbeatIntervalMs: 3e4,
645
- tokenRefreshIntervalMs: 45 * 60 * 1e3
646
+ tokenRefreshIntervalMs: 45 * 60 * 1e3,
647
+ gitFlushIntervalMs: 2 * 60 * 1e3
646
648
  };
647
649
  var Lifecycle = class {
648
650
  config;
@@ -651,6 +653,8 @@ var Lifecycle = class {
651
653
  tokenRefreshTimer = null;
652
654
  idleTimer = null;
653
655
  idleCheckInterval = null;
656
+ dormantTimer = null;
657
+ gitFlushTimer = null;
654
658
  constructor(config, callbacks) {
655
659
  this.config = config;
656
660
  this.callbacks = callbacks;
@@ -682,6 +686,19 @@ var Lifecycle = class {
682
686
  this.tokenRefreshTimer = null;
683
687
  }
684
688
  }
689
+ // ── Periodic git flush ────────────────────────────────────────────
690
+ startGitFlush() {
691
+ this.stopGitFlush();
692
+ this.gitFlushTimer = setInterval(() => {
693
+ this.callbacks.onGitFlush();
694
+ }, this.config.gitFlushIntervalMs);
695
+ }
696
+ stopGitFlush() {
697
+ if (this.gitFlushTimer) {
698
+ clearInterval(this.gitFlushTimer);
699
+ this.gitFlushTimer = null;
700
+ }
701
+ }
685
702
  // ── Idle timer ─────────────────────────────────────────────────────
686
703
  startIdleTimer() {
687
704
  this.clearIdleTimers();
@@ -692,11 +709,34 @@ var Lifecycle = class {
692
709
  cancelIdleTimer() {
693
710
  this.clearIdleTimers();
694
711
  }
712
+ // ── Dormant timer ──────────────────────────────────────────────────
713
+ /** Start (or restart) the dormant timer.
714
+ * @param overrideMs Optional custom delay in ms. When provided, the timer
715
+ * fires after exactly that delay instead of `dormantTimeoutMs`. SessionRunner
716
+ * uses this to enforce an *absolute* deadline across cycles: even if the
717
+ * dormant wait is interrupted by an inbound message, the next iteration
718
+ * passes the remaining time, so the agent shuts down at the original
719
+ * deadline regardless of message volume. */
720
+ startDormantTimer(overrideMs) {
721
+ this.cancelDormantTimer();
722
+ const delay = Math.max(0, overrideMs ?? this.config.dormantTimeoutMs);
723
+ this.dormantTimer = setTimeout(() => {
724
+ this.callbacks.onDormantTimeout();
725
+ }, delay);
726
+ }
727
+ cancelDormantTimer() {
728
+ if (this.dormantTimer) {
729
+ clearTimeout(this.dormantTimer);
730
+ this.dormantTimer = null;
731
+ }
732
+ }
695
733
  // ── Cleanup ────────────────────────────────────────────────────────
696
734
  destroy() {
697
735
  this.stopHeartbeat();
698
736
  this.stopTokenRefresh();
737
+ this.stopGitFlush();
699
738
  this.clearIdleTimers();
739
+ this.cancelDormantTimer();
700
740
  }
701
741
  // ── Private ────────────────────────────────────────────────────────
702
742
  clearIdleTimers() {
@@ -1773,6 +1813,20 @@ function formatRepoRefs(repoRefs) {
1773
1813
  }
1774
1814
  return parts;
1775
1815
  }
1816
+ function formatReferenceProjects(referenceProjects) {
1817
+ const parts = [];
1818
+ parts.push(`
1819
+ ## Reference Projects`);
1820
+ parts.push(
1821
+ `These sibling Conveyor projects have been shallow-cloned read-only into \`/workspaces/references/<slug>/\` for inspiration. You MAY grep/read them to compare approaches, but you MUST NOT modify them or commit anything from them into this task's repo.
1822
+ `
1823
+ );
1824
+ for (const ref of referenceProjects) {
1825
+ const repo = ref.githubRepoOwner && ref.githubRepoName ? ` (${ref.githubRepoOwner}/${ref.githubRepoName})` : "";
1826
+ parts.push(`- **${ref.name}**${repo} \u2014 \`/workspaces/references/${ref.slug}/\``);
1827
+ }
1828
+ return parts;
1829
+ }
1776
1830
  function formatProjectObjectives(objectives) {
1777
1831
  const parts = [];
1778
1832
  parts.push(`
@@ -2686,6 +2740,9 @@ ${truncatePlanForPrompt(context.plan)}`);
2686
2740
  if (context.repoRefs && context.repoRefs.length > 0) {
2687
2741
  parts.push(...formatRepoRefs(context.repoRefs));
2688
2742
  }
2743
+ if (context.referenceProjects && context.referenceProjects.length > 0) {
2744
+ parts.push(...formatReferenceProjects(context.referenceProjects));
2745
+ }
2689
2746
  const tagSection = await resolveTaskTagContext(context, runnerMode);
2690
2747
  if (tagSection) parts.push(tagSection);
2691
2748
  if (runnerMode !== "task") {
@@ -6528,12 +6585,21 @@ var SessionRunner = class _SessionRunner {
6528
6585
  /** Defense-in-depth: set when the agent emits a "completed" event.
6529
6586
  * Prevents the core loop from processing any further messages. */
6530
6587
  completedThisTurn = false;
6588
+ /** Absolute deadline (ms epoch) at which the dormant idle wait must time
6589
+ * out. Set on the FIRST entry into dormant idle for a given completion
6590
+ * cycle and preserved across iterations so inbound critical messages
6591
+ * cannot extend the bound past the configured `dormantTimeoutMs`. Reset
6592
+ * to null when the agent transitions out of dormant idle (a wake actually
6593
+ * promotes it back to a working turn). */
6594
+ dormantDeadline = null;
6531
6595
  taskContext = null;
6532
6596
  fullContext = null;
6533
6597
  queryBridge = null;
6534
6598
  inputResolver = null;
6535
6599
  pendingMessages = [];
6536
6600
  prNudgeCount = 0;
6601
+ /** Guards overlapping runs of the periodic git flush. */
6602
+ periodicFlushInFlight = false;
6537
6603
  constructor(config, callbacks) {
6538
6604
  this.config = config;
6539
6605
  this.callbacks = callbacks;
@@ -6552,7 +6618,17 @@ var SessionRunner = class _SessionRunner {
6552
6618
  resolver(null);
6553
6619
  }
6554
6620
  },
6555
- onTokenRefresh: () => void this.refreshGithubToken()
6621
+ onDormantTimeout: () => {
6622
+ process.stderr.write("[conveyor-agent] Dormant idle timeout reached, shutting down\n");
6623
+ this.stopped = true;
6624
+ if (this.inputResolver) {
6625
+ const resolver = this.inputResolver;
6626
+ this.inputResolver = null;
6627
+ resolver(null);
6628
+ }
6629
+ },
6630
+ onTokenRefresh: () => void this.refreshGithubToken(),
6631
+ onGitFlush: () => void this.periodicGitFlush()
6556
6632
  });
6557
6633
  }
6558
6634
  get state() {
@@ -6578,6 +6654,7 @@ var SessionRunner = class _SessionRunner {
6578
6654
  this.wireConnectionCallbacks();
6579
6655
  this.lifecycle.startHeartbeat();
6580
6656
  this.lifecycle.startTokenRefresh();
6657
+ this.lifecycle.startGitFlush();
6581
6658
  const { pendingMessages: serverMessages } = await this.connection.call("connectAgent", {
6582
6659
  sessionId: this.sessionId
6583
6660
  });
@@ -6667,16 +6744,8 @@ var SessionRunner = class _SessionRunner {
6667
6744
  async coreLoop() {
6668
6745
  while (!this.stopped) {
6669
6746
  if (this.completedThisTurn) {
6670
- process.stderr.write(
6671
- "[conveyor-agent] Completed \u2014 entering dormant idle (staying connected)\n"
6672
- );
6673
- this.pendingMessages.length = 0;
6674
- if (this._state !== "idle") await this.setState("idle");
6675
- const dormantMsg = await this.waitForMessage();
6676
- if (!dormantMsg) break;
6677
- process.stderr.write("[conveyor-agent] Received message while dormant, resuming\n");
6678
- this.completedThisTurn = false;
6679
- this.pendingMessages.unshift(dormantMsg);
6747
+ const resumed = await this.handleDormantIdle();
6748
+ if (!resumed) break;
6680
6749
  continue;
6681
6750
  }
6682
6751
  if (this._state === "idle") {
@@ -6730,6 +6799,41 @@ var SessionRunner = class _SessionRunner {
6730
6799
  }
6731
6800
  }
6732
6801
  }
6802
+ /**
6803
+ * Handle dormant-after-completed idle. Returns true if a critical message
6804
+ * woke us (caller should `continue` the loop), false if we should break
6805
+ * (stop signal or dormant timeout fired).
6806
+ *
6807
+ * The absolute deadline is set on first entry per completion cycle and
6808
+ * preserved across re-entries: an inbound critical message cannot extend
6809
+ * the bound past `dormantTimeoutMs`. Only a genuine wake (clearing
6810
+ * `completedThisTurn`) resets the deadline so the next dormant entry
6811
+ * starts a fresh window.
6812
+ */
6813
+ async handleDormantIdle() {
6814
+ if (this.dormantDeadline === null) {
6815
+ this.dormantDeadline = Date.now() + this.lifecycle.config.dormantTimeoutMs;
6816
+ process.stderr.write(
6817
+ "[conveyor-agent] Completed \u2014 entering dormant idle (staying connected)\n"
6818
+ );
6819
+ }
6820
+ this.pendingMessages.length = 0;
6821
+ if (this._state !== "idle") await this.setState("idle");
6822
+ const remainingMs = Math.max(0, this.dormantDeadline - Date.now());
6823
+ this.lifecycle.startDormantTimer(remainingMs);
6824
+ const dormantMsg = await this.waitForMessage();
6825
+ this.lifecycle.cancelDormantTimer();
6826
+ if (!dormantMsg) return false;
6827
+ const contentPreview = dormantMsg.content.length > 80 ? `${dormantMsg.content.slice(0, 80)}...` : dormantMsg.content;
6828
+ process.stderr.write(
6829
+ `[conveyor-agent] Received message while dormant, resuming: userId=${dormantMsg.userId}, source=${dormantMsg.source || "unknown"}, content="${contentPreview.replace(/\n/g, "\\n")}"
6830
+ `
6831
+ );
6832
+ this.completedThisTurn = false;
6833
+ this.dormantDeadline = null;
6834
+ this.pendingMessages.unshift(dormantMsg);
6835
+ return true;
6836
+ }
6733
6837
  // ── Initial mode execution ─────────────────────────────────────────
6734
6838
  /** Returns true if an initial query was executed, false otherwise. */
6735
6839
  async executeInitialMode() {
@@ -6783,6 +6887,38 @@ var SessionRunner = class _SessionRunner {
6783
6887
  }
6784
6888
  }
6785
6889
  // ── Stop / soft-stop ───────────────────────────────────────────────
6890
+ /** Periodic best-effort WIP commit + push during normal agent execution.
6891
+ * Covers ungraceful pod termination (OOMKilled, node crash/eviction) where
6892
+ * the preStop hook + SIGTERM flush don't get a chance to run. No-ops on a
6893
+ * clean tree. Guarded so two ticks can't overlap. Never throws. */
6894
+ async periodicGitFlush() {
6895
+ if (this.periodicFlushInFlight || this.stopped) return;
6896
+ this.periodicFlushInFlight = true;
6897
+ try {
6898
+ const result = await flushPendingChanges(this.config.workspaceDir, {
6899
+ wipMessage: "WIP: periodic auto-commit",
6900
+ refreshToken: async () => {
6901
+ try {
6902
+ const res = await this.connection.call("refreshGithubToken", {
6903
+ sessionId: this.connection.sessionId
6904
+ });
6905
+ return res.token;
6906
+ } catch {
6907
+ return void 0;
6908
+ }
6909
+ }
6910
+ });
6911
+ if (result.hadWork) {
6912
+ process.stderr.write(
6913
+ `[conveyor-agent] Periodic git flush: committed=${result.committed} pushed=${result.pushed}
6914
+ `
6915
+ );
6916
+ }
6917
+ } catch {
6918
+ } finally {
6919
+ this.periodicFlushInFlight = false;
6920
+ }
6921
+ }
6786
6922
  /** Best-effort WIP commit + push on shutdown so in-flight work isn't lost
6787
6923
  * when a claudespace pod is killed. Must be called BEFORE stop() so the
6788
6924
  * connection is still alive for token refresh. Never throws. */
@@ -6944,6 +7080,7 @@ var SessionRunner = class _SessionRunner {
6944
7080
  onEvent: (event) => {
6945
7081
  if (event.type === "completed") {
6946
7082
  this.completedThisTurn = true;
7083
+ void this.connection.sendHeartbeat();
6947
7084
  }
6948
7085
  return this.callbacks.onEvent(event);
6949
7086
  }
@@ -7080,23 +7217,30 @@ var SessionRunner = class _SessionRunner {
7080
7217
  get finalState() {
7081
7218
  return this._finalState;
7082
7219
  }
7083
- logInitialization() {
7084
- const context = {
7085
- mode: this.mode.effectiveMode,
7086
- runnerMode: this.config.runnerMode ?? "task",
7087
- sessionId: this.sessionId,
7088
- // Task context
7220
+ buildTaskContextSnapshot() {
7221
+ return {
7089
7222
  isParentTask: this.fullContext?.isParentTask ?? false,
7090
7223
  status: this.taskContext?.status,
7091
7224
  taskTitle: this.fullContext?.title,
7092
7225
  hasExistingPR: !!this.fullContext?.githubPRUrl,
7093
7226
  hasExistingSession: !!this.fullContext?.claudeSessionId,
7094
7227
  chatHistoryLength: this.fullContext?.chatHistory?.length ?? 0,
7095
- tagIds: this.fullContext?.taskTagIds ?? [],
7096
- // Agent config
7228
+ tagIds: this.fullContext?.taskTagIds ?? []
7229
+ };
7230
+ }
7231
+ buildInitializationContext() {
7232
+ return {
7233
+ mode: this.mode.effectiveMode,
7234
+ runnerMode: this.config.runnerMode ?? "task",
7235
+ sessionId: this.sessionId,
7236
+ ...this.buildTaskContextSnapshot(),
7097
7237
  model: this.taskContext?.model,
7098
- isAuto: this.config.isAuto ?? false
7238
+ isAuto: this.config.isAuto ?? false,
7239
+ subscriptionKeyLabel: process.env.CONVEYOR_SUBSCRIPTION_KEY_LABEL ?? null
7099
7240
  };
7241
+ }
7242
+ logInitialization() {
7243
+ const context = this.buildInitializationContext();
7100
7244
  process.stderr.write(`[conveyor-agent] Initialized: ${JSON.stringify(context)}
7101
7245
  `);
7102
7246
  this.connection.sendEvent({ type: "session_manifest", ...context });
@@ -8586,15 +8730,38 @@ var ProjectRunner = class {
8586
8730
  import { readFile as readFile2 } from "fs/promises";
8587
8731
  import { join as join5 } from "path";
8588
8732
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
8733
+ var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
8589
8734
  async function loadForwardPorts(workspaceDir) {
8590
8735
  try {
8591
8736
  const raw = await readFile2(join5(workspaceDir, DEVCONTAINER_PATH), "utf-8");
8592
8737
  const parsed = JSON.parse(raw);
8593
- return parsed.forwardPorts ?? [];
8738
+ const ports = (parsed.forwardPorts ?? []).filter(
8739
+ (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
8740
+ );
8741
+ const attributes = {};
8742
+ for (const [key, value] of Object.entries(parsed.portsAttributes ?? {})) {
8743
+ if (!value || typeof value !== "object") continue;
8744
+ const entry = {};
8745
+ if (typeof value.label === "string") entry.label = value.label;
8746
+ if (value.visibility === "public" || value.visibility === "private") {
8747
+ entry.visibility = value.visibility;
8748
+ }
8749
+ attributes[key] = entry;
8750
+ }
8751
+ return { ports, attributes };
8594
8752
  } catch {
8595
- return [];
8753
+ return { ports: [], attributes: {} };
8596
8754
  }
8597
8755
  }
8756
+ function buildSessionPreviewPorts(result) {
8757
+ return result.ports.filter((port) => !DEVCONTAINER_PORT_DENY_LIST.has(port)).map((port) => {
8758
+ const attr = result.attributes[String(port)];
8759
+ const entry = { port };
8760
+ if (attr?.label) entry.label = attr.label;
8761
+ if (attr?.visibility) entry.visibility = attr.visibility;
8762
+ return entry;
8763
+ });
8764
+ }
8598
8765
  function loadConveyorConfig() {
8599
8766
  const envSetup = process.env.CONVEYOR_SETUP_COMMAND;
8600
8767
  const envStart = process.env.CONVEYOR_START_COMMAND;
@@ -8634,6 +8801,7 @@ export {
8634
8801
  runStartCommand,
8635
8802
  ProjectRunner,
8636
8803
  loadForwardPorts,
8804
+ buildSessionPreviewPorts,
8637
8805
  loadConveyorConfig
8638
8806
  };
8639
- //# sourceMappingURL=chunk-TP5WEBQE.js.map
8807
+ //# sourceMappingURL=chunk-PC43BKMM.js.map