@rallycry/conveyor-agent 7.3.4 → 7.3.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.
@@ -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,26 @@ var Lifecycle = class {
692
709
  cancelIdleTimer() {
693
710
  this.clearIdleTimers();
694
711
  }
712
+ // ── Dormant timer ──────────────────────────────────────────────────
713
+ startDormantTimer() {
714
+ this.cancelDormantTimer();
715
+ this.dormantTimer = setTimeout(() => {
716
+ this.callbacks.onDormantTimeout();
717
+ }, this.config.dormantTimeoutMs);
718
+ }
719
+ cancelDormantTimer() {
720
+ if (this.dormantTimer) {
721
+ clearTimeout(this.dormantTimer);
722
+ this.dormantTimer = null;
723
+ }
724
+ }
695
725
  // ── Cleanup ────────────────────────────────────────────────────────
696
726
  destroy() {
697
727
  this.stopHeartbeat();
698
728
  this.stopTokenRefresh();
729
+ this.stopGitFlush();
699
730
  this.clearIdleTimers();
731
+ this.cancelDormantTimer();
700
732
  }
701
733
  // ── Private ────────────────────────────────────────────────────────
702
734
  clearIdleTimers() {
@@ -1773,6 +1805,20 @@ function formatRepoRefs(repoRefs) {
1773
1805
  }
1774
1806
  return parts;
1775
1807
  }
1808
+ function formatReferenceProjects(referenceProjects) {
1809
+ const parts = [];
1810
+ parts.push(`
1811
+ ## Reference Projects`);
1812
+ parts.push(
1813
+ `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.
1814
+ `
1815
+ );
1816
+ for (const ref of referenceProjects) {
1817
+ const repo = ref.githubRepoOwner && ref.githubRepoName ? ` (${ref.githubRepoOwner}/${ref.githubRepoName})` : "";
1818
+ parts.push(`- **${ref.name}**${repo} \u2014 \`/workspaces/references/${ref.slug}/\``);
1819
+ }
1820
+ return parts;
1821
+ }
1776
1822
  function formatProjectObjectives(objectives) {
1777
1823
  const parts = [];
1778
1824
  parts.push(`
@@ -2686,6 +2732,9 @@ ${truncatePlanForPrompt(context.plan)}`);
2686
2732
  if (context.repoRefs && context.repoRefs.length > 0) {
2687
2733
  parts.push(...formatRepoRefs(context.repoRefs));
2688
2734
  }
2735
+ if (context.referenceProjects && context.referenceProjects.length > 0) {
2736
+ parts.push(...formatReferenceProjects(context.referenceProjects));
2737
+ }
2689
2738
  const tagSection = await resolveTaskTagContext(context, runnerMode);
2690
2739
  if (tagSection) parts.push(tagSection);
2691
2740
  if (runnerMode !== "task") {
@@ -6534,6 +6583,8 @@ var SessionRunner = class _SessionRunner {
6534
6583
  inputResolver = null;
6535
6584
  pendingMessages = [];
6536
6585
  prNudgeCount = 0;
6586
+ /** Guards overlapping runs of the periodic git flush. */
6587
+ periodicFlushInFlight = false;
6537
6588
  constructor(config, callbacks) {
6538
6589
  this.config = config;
6539
6590
  this.callbacks = callbacks;
@@ -6552,7 +6603,17 @@ var SessionRunner = class _SessionRunner {
6552
6603
  resolver(null);
6553
6604
  }
6554
6605
  },
6555
- onTokenRefresh: () => void this.refreshGithubToken()
6606
+ onDormantTimeout: () => {
6607
+ process.stderr.write("[conveyor-agent] Dormant idle timeout reached, shutting down\n");
6608
+ this.stopped = true;
6609
+ if (this.inputResolver) {
6610
+ const resolver = this.inputResolver;
6611
+ this.inputResolver = null;
6612
+ resolver(null);
6613
+ }
6614
+ },
6615
+ onTokenRefresh: () => void this.refreshGithubToken(),
6616
+ onGitFlush: () => void this.periodicGitFlush()
6556
6617
  });
6557
6618
  }
6558
6619
  get state() {
@@ -6578,6 +6639,7 @@ var SessionRunner = class _SessionRunner {
6578
6639
  this.wireConnectionCallbacks();
6579
6640
  this.lifecycle.startHeartbeat();
6580
6641
  this.lifecycle.startTokenRefresh();
6642
+ this.lifecycle.startGitFlush();
6581
6643
  const { pendingMessages: serverMessages } = await this.connection.call("connectAgent", {
6582
6644
  sessionId: this.sessionId
6583
6645
  });
@@ -6672,9 +6734,15 @@ var SessionRunner = class _SessionRunner {
6672
6734
  );
6673
6735
  this.pendingMessages.length = 0;
6674
6736
  if (this._state !== "idle") await this.setState("idle");
6737
+ this.lifecycle.startDormantTimer();
6675
6738
  const dormantMsg = await this.waitForMessage();
6739
+ this.lifecycle.cancelDormantTimer();
6676
6740
  if (!dormantMsg) break;
6677
- process.stderr.write("[conveyor-agent] Received message while dormant, resuming\n");
6741
+ const contentPreview = dormantMsg.content.length > 80 ? `${dormantMsg.content.slice(0, 80)}...` : dormantMsg.content;
6742
+ process.stderr.write(
6743
+ `[conveyor-agent] Received message while dormant, resuming: userId=${dormantMsg.userId}, source=${dormantMsg.source || "unknown"}, content="${contentPreview.replace(/\n/g, "\\n")}"
6744
+ `
6745
+ );
6678
6746
  this.completedThisTurn = false;
6679
6747
  this.pendingMessages.unshift(dormantMsg);
6680
6748
  continue;
@@ -6783,6 +6851,38 @@ var SessionRunner = class _SessionRunner {
6783
6851
  }
6784
6852
  }
6785
6853
  // ── Stop / soft-stop ───────────────────────────────────────────────
6854
+ /** Periodic best-effort WIP commit + push during normal agent execution.
6855
+ * Covers ungraceful pod termination (OOMKilled, node crash/eviction) where
6856
+ * the preStop hook + SIGTERM flush don't get a chance to run. No-ops on a
6857
+ * clean tree. Guarded so two ticks can't overlap. Never throws. */
6858
+ async periodicGitFlush() {
6859
+ if (this.periodicFlushInFlight || this.stopped) return;
6860
+ this.periodicFlushInFlight = true;
6861
+ try {
6862
+ const result = await flushPendingChanges(this.config.workspaceDir, {
6863
+ wipMessage: "WIP: periodic auto-commit",
6864
+ refreshToken: async () => {
6865
+ try {
6866
+ const res = await this.connection.call("refreshGithubToken", {
6867
+ sessionId: this.connection.sessionId
6868
+ });
6869
+ return res.token;
6870
+ } catch {
6871
+ return void 0;
6872
+ }
6873
+ }
6874
+ });
6875
+ if (result.hadWork) {
6876
+ process.stderr.write(
6877
+ `[conveyor-agent] Periodic git flush: committed=${result.committed} pushed=${result.pushed}
6878
+ `
6879
+ );
6880
+ }
6881
+ } catch {
6882
+ } finally {
6883
+ this.periodicFlushInFlight = false;
6884
+ }
6885
+ }
6786
6886
  /** Best-effort WIP commit + push on shutdown so in-flight work isn't lost
6787
6887
  * when a claudespace pod is killed. Must be called BEFORE stop() so the
6788
6888
  * connection is still alive for token refresh. Never throws. */
@@ -6944,6 +7044,7 @@ var SessionRunner = class _SessionRunner {
6944
7044
  onEvent: (event) => {
6945
7045
  if (event.type === "completed") {
6946
7046
  this.completedThisTurn = true;
7047
+ void this.connection.sendHeartbeat();
6947
7048
  }
6948
7049
  return this.callbacks.onEvent(event);
6949
7050
  }
@@ -7080,23 +7181,30 @@ var SessionRunner = class _SessionRunner {
7080
7181
  get finalState() {
7081
7182
  return this._finalState;
7082
7183
  }
7083
- logInitialization() {
7084
- const context = {
7085
- mode: this.mode.effectiveMode,
7086
- runnerMode: this.config.runnerMode ?? "task",
7087
- sessionId: this.sessionId,
7088
- // Task context
7184
+ buildTaskContextSnapshot() {
7185
+ return {
7089
7186
  isParentTask: this.fullContext?.isParentTask ?? false,
7090
7187
  status: this.taskContext?.status,
7091
7188
  taskTitle: this.fullContext?.title,
7092
7189
  hasExistingPR: !!this.fullContext?.githubPRUrl,
7093
7190
  hasExistingSession: !!this.fullContext?.claudeSessionId,
7094
7191
  chatHistoryLength: this.fullContext?.chatHistory?.length ?? 0,
7095
- tagIds: this.fullContext?.taskTagIds ?? [],
7096
- // Agent config
7192
+ tagIds: this.fullContext?.taskTagIds ?? []
7193
+ };
7194
+ }
7195
+ buildInitializationContext() {
7196
+ return {
7197
+ mode: this.mode.effectiveMode,
7198
+ runnerMode: this.config.runnerMode ?? "task",
7199
+ sessionId: this.sessionId,
7200
+ ...this.buildTaskContextSnapshot(),
7097
7201
  model: this.taskContext?.model,
7098
- isAuto: this.config.isAuto ?? false
7202
+ isAuto: this.config.isAuto ?? false,
7203
+ subscriptionKeyLabel: process.env.CONVEYOR_SUBSCRIPTION_KEY_LABEL ?? null
7099
7204
  };
7205
+ }
7206
+ logInitialization() {
7207
+ const context = this.buildInitializationContext();
7100
7208
  process.stderr.write(`[conveyor-agent] Initialized: ${JSON.stringify(context)}
7101
7209
  `);
7102
7210
  this.connection.sendEvent({ type: "session_manifest", ...context });
@@ -8586,15 +8694,38 @@ var ProjectRunner = class {
8586
8694
  import { readFile as readFile2 } from "fs/promises";
8587
8695
  import { join as join5 } from "path";
8588
8696
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
8697
+ var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
8589
8698
  async function loadForwardPorts(workspaceDir) {
8590
8699
  try {
8591
8700
  const raw = await readFile2(join5(workspaceDir, DEVCONTAINER_PATH), "utf-8");
8592
8701
  const parsed = JSON.parse(raw);
8593
- return parsed.forwardPorts ?? [];
8702
+ const ports = (parsed.forwardPorts ?? []).filter(
8703
+ (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
8704
+ );
8705
+ const attributes = {};
8706
+ for (const [key, value] of Object.entries(parsed.portsAttributes ?? {})) {
8707
+ if (!value || typeof value !== "object") continue;
8708
+ const entry = {};
8709
+ if (typeof value.label === "string") entry.label = value.label;
8710
+ if (value.visibility === "public" || value.visibility === "private") {
8711
+ entry.visibility = value.visibility;
8712
+ }
8713
+ attributes[key] = entry;
8714
+ }
8715
+ return { ports, attributes };
8594
8716
  } catch {
8595
- return [];
8717
+ return { ports: [], attributes: {} };
8596
8718
  }
8597
8719
  }
8720
+ function buildSessionPreviewPorts(result) {
8721
+ return result.ports.filter((port) => !DEVCONTAINER_PORT_DENY_LIST.has(port)).map((port) => {
8722
+ const attr = result.attributes[String(port)];
8723
+ const entry = { port };
8724
+ if (attr?.label) entry.label = attr.label;
8725
+ if (attr?.visibility) entry.visibility = attr.visibility;
8726
+ return entry;
8727
+ });
8728
+ }
8598
8729
  function loadConveyorConfig() {
8599
8730
  const envSetup = process.env.CONVEYOR_SETUP_COMMAND;
8600
8731
  const envStart = process.env.CONVEYOR_START_COMMAND;
@@ -8634,6 +8765,7 @@ export {
8634
8765
  runStartCommand,
8635
8766
  ProjectRunner,
8636
8767
  loadForwardPorts,
8768
+ buildSessionPreviewPorts,
8637
8769
  loadConveyorConfig
8638
8770
  };
8639
- //# sourceMappingURL=chunk-TP5WEBQE.js.map
8771
+ //# sourceMappingURL=chunk-KO2YQJEV.js.map