Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions scripts/comprehensive-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,12 @@ async function testActionRoutingAndSafety() {
await h.command("loop-clear")
await h.command("loop-shell", "0s --safe rm -r -f ./important")
await h.command("loop-now", "shell")
const separateFlagsState = await h.readState()
const separateFlagsState = await waitForValue(async () => {
const candidate = await h.readState()
return candidate.jobs[0]?.lastFailureReason === "safe_shell_blocked" ? candidate : undefined
}, 2_000)
assert.equal(h.records.shells.length, 0)
assert.equal(separateFlagsState.jobs[0].lastFailureReason, "safe_shell_blocked", "separate rm -r -f flags must be blocked")
assert.equal(separateFlagsState?.jobs[0]?.lastFailureReason, "safe_shell_blocked", "separate rm -r -f flags must be blocked")
} finally {
await h.cleanup()
}
Expand Down
16 changes: 16 additions & 0 deletions scripts/loop-command-handlers-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,22 @@ assert.throws(() => createLoopCommandHandlers({ clearActiveRun() {} }), /cancelD
assert.match(text, /goal:blocked,paused,checkpoint-only,git-checkpoint/)
}

{
const sessionID = "status-no-now"
const h = harness({
[sessionID]: {
jobs: [loopJob("delayed", {
intervalMs: 10_000,
lastRunAt: 0,
immediate: false,
createdAt: new Date(5_000).toISOString(),
})],
},
}, { now: () => 10_000 })
await h.handlers.statusLoop("/work", {}, sessionID)
assert.match(h.messages[0][2], /due in 5s/, "--no-now status must count the first interval from createdAt")
}

{
const sessionID = "status-empty"
const h = harness({ [sessionID]: { jobs: [] } })
Expand Down
7 changes: 6 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1698,7 +1698,12 @@ function createLoopCommandHandlers(options = {}) {
const state = await readState2(directory, sessionID);
const jobs = state.jobs || [];
const lines = jobs.length ? jobs.map((job, index) => {
const dueIn = Number(job.runNowRequestedAt || 0) > 0 ? 0 : Math.max(0, job.intervalMs - (now2() - (job.lastRunAt || 0)));
const current = now2();
const intervalMs = Number(job.intervalMs || 0);
const lastRunAt = Number(job.lastRunAt || 0);
const createdAt = Date.parse(job.createdAt || "");
const dueAt = Number(job.runNowRequestedAt || 0) > 0 ? current : lastRunAt > 0 ? lastRunAt + intervalMs : job.immediate === false && Number.isFinite(createdAt) ? createdAt + intervalMs : current;
const dueIn = Math.max(0, dueAt - current);
const flags = [isGoalJob(job) ? `goal:${goalStatusText(job)}` : undefined, job.paused ? "paused" : "active", Number(job.runNowRequestedAt || 0) > 0 ? "run-now" : undefined, job.safe ? "safe" : undefined, job.askNever ? "ask-never" : undefined, job.noOverlap ? "no-overlap" : undefined, job.checkpointOnly ? "checkpoint-only" : undefined, job.gitCheckpoint ? "git-checkpoint" : undefined].filter(Boolean).join(",");
return `${index + 1}. ${job.id}${job.name ? ` (${job.name})` : ""}: ${jobLabel(job)} | runs=${job.runCount || 0} | failures=${job.failureCount || 0} | due in ${durationToText(dueIn)} | ${flags}`;
}) : ["No active loop jobs."];
Expand Down
13 changes: 12 additions & 1 deletion src/source/opencode/loop-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,18 @@ export function createLoopCommandHandlers(options = {}) {
const state = await readState(directory, sessionID)
const jobs = state.jobs || []
const lines = jobs.length ? jobs.map((job, index) => {
const dueIn = Number(job.runNowRequestedAt || 0) > 0 ? 0 : Math.max(0, job.intervalMs - (now() - (job.lastRunAt || 0)))
const current = now()
const intervalMs = Number(job.intervalMs || 0)
const lastRunAt = Number(job.lastRunAt || 0)
const createdAt = Date.parse(job.createdAt || "")
const dueAt = Number(job.runNowRequestedAt || 0) > 0
? current
: lastRunAt > 0
? lastRunAt + intervalMs
: job.immediate === false && Number.isFinite(createdAt)
? createdAt + intervalMs
: current
const dueIn = Math.max(0, dueAt - current)
const flags = [isGoalJob(job) ? `goal:${goalStatusText(job)}` : undefined, job.paused ? "paused" : "active", Number(job.runNowRequestedAt || 0) > 0 ? "run-now" : undefined, job.safe ? "safe" : undefined, job.askNever ? "ask-never" : undefined, job.noOverlap ? "no-overlap" : undefined, job.checkpointOnly ? "checkpoint-only" : undefined, job.gitCheckpoint ? "git-checkpoint" : undefined].filter(Boolean).join(",")
return `${index + 1}. ${job.id}${job.name ? ` (${job.name})` : ""}: ${jobLabel(job)} | runs=${job.runCount || 0} | failures=${job.failureCount || 0} | due in ${durationToText(dueIn)} | ${flags}`
}) : ["No active loop jobs."]
Expand Down