@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.
- package/dist/{chunk-TP5WEBQE.js → chunk-PC43BKMM.js} +192 -24
- package/dist/chunk-PC43BKMM.js.map +1 -0
- package/dist/cli.js +20 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +60 -3
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-TP5WEBQE.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
6671
|
-
|
|
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
|
-
|
|
7084
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
8807
|
+
//# sourceMappingURL=chunk-PC43BKMM.js.map
|