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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ new version heading in the same commit.

## [Unreleased]

## [0.450.1] - 2026-10-03
### Fixed
- **An interactive cron no longer stops firing because its last run is still open.** An interactive run
keeps its TUI after it finishes, and the scheduler's pile-up guard read "pane alive" as "still running",
so every later occurrence was skipped — silently, with no audit — until someone closed the tab (the
console said as much: "this cron won't re-fire while its last run is live"). The next occurrence now
closes the previous run when nobody is using it (`TerminalManager.supersedeIdleRun`: detached, unclaimed,
no turn in flight, nothing waiting on a person) — episode written, audited `automation.superseded` +
`session.reaped` `reason: superseded` — and skips only while it is genuinely in use. A skip is now
audited once per occurrence (`automation.skipped`, with the reason). Pinned by
`scripts/cron-supersede-test.cjs`.
**For users:** Interactive scheduled automations now run on every occurrence — the new run closes the previous one if you've left it idle, and only waits while you're still using it.

## [0.450.0] - 2026-10-03
### Added
- **Login-expiry warnings for Claude Code credentials.** Every `claude login` has a fixed refresh-token
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-os",
"version": "0.450.0",
"version": "0.450.1",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
Expand All @@ -27,7 +27,7 @@
"check-deps": "bash scripts/install-deps.sh --check",
"dev": "ts-node src/cli.ts serve",
"demo:dev": "ts-node src/demo.ts",
"test:governance": "node scripts/version-sync-test.cjs && node scripts/event-loop-freeze-test.cjs && node scripts/git-ceiling-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/script-body-gate-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/capacity-queue-test.cjs && node scripts/runtime-pool-signed-out-test.cjs && node scripts/login-expiry-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/session-pause-test.cjs && node scripts/make-live-lock-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/task-unblock-test.cjs && node scripts/audience-session-access-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/task-proposals-test.cjs && node scripts/npm-boundary-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/github-multi-org-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/whats-new-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/drift-nudge-test.cjs && node scripts/goal-metric-review-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs && node scripts/automation-edit-proposal-test.cjs && node scripts/app-card-close-test.cjs",
"test:governance": "node scripts/version-sync-test.cjs && node scripts/event-loop-freeze-test.cjs && node scripts/git-ceiling-test.cjs && node scripts/cron-supersede-test.cjs && node scripts/governance-conformance.cjs && node scripts/tier-a-policy-test.cjs && node scripts/policy-baseline-test.cjs && node scripts/heredoc-intent-test.cjs && node scripts/script-body-gate-test.cjs && node scripts/capability-registry-test.cjs && node scripts/composio-envelope-test.cjs && node scripts/composio-identity-test.cjs && node scripts/idle-reaper-test.cjs && node scripts/dm-continuity-test.cjs && node scripts/telegram-dm-lane-test.cjs && node scripts/cli-link-origin-test.cjs && node scripts/alert-staleness-test.cjs && node scripts/run-as-identity-test.cjs && node scripts/email-identity-guard-test.cjs && node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-usage-refresh-test.cjs && node scripts/keychain-credential-test.cjs && node scripts/credential-preflight-test.cjs && node scripts/capacity-queue-test.cjs && node scripts/runtime-pool-signed-out-test.cjs && node scripts/login-expiry-test.cjs && node scripts/runtime-login-test.cjs && node scripts/rotate-on-reload-test.cjs && node scripts/headless-resumable-test.cjs && node scripts/session-revive-gates-test.cjs && node scripts/session-pause-test.cjs && node scripts/make-live-lock-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs && node scripts/output-style-test.cjs && node scripts/session-cost-test.cjs && node scripts/chain-model-test.cjs && node scripts/task-workers-test.cjs && node scripts/tuning-patch-test.cjs && node scripts/task-runs-test.cjs && node scripts/task-pr-links-test.cjs && node scripts/task-draft-delete-test.cjs && node scripts/task-discussion-delivery-test.cjs && node scripts/task-resume-test.cjs && node scripts/task-unblock-test.cjs && node scripts/audience-session-access-test.cjs && node scripts/warm-chat-test.cjs && node scripts/poke-warm-caller-test.cjs && node scripts/wakeup-queue-test.cjs && node scripts/stranded-human-stop-test.cjs && node scripts/inject-submit-test.cjs && node scripts/blocked-routing-test.cjs && node scripts/self-dispatch-guard-test.cjs && node scripts/task-proposals-test.cjs && node scripts/npm-boundary-test.cjs && node scripts/agent-edit-guard-test.cjs && node scripts/per-agent-context-test.cjs && node scripts/goal-update-guard-test.cjs && node scripts/insights-signal-test.cjs && node scripts/outcome-derivation-test.cjs && node scripts/episode-quality-test.cjs && node scripts/memory-upkeep-test.cjs && node scripts/automem-health-test.cjs && node scripts/memory-store-switch-test.cjs && node scripts/memory-preload-test.cjs && node scripts/turn-lifecycle-test.cjs && node scripts/resume-seed-test.cjs && node scripts/outcome-vocabulary-test.cjs && node scripts/skill-presets-test.cjs && node scripts/skill-edit-proposal-test.cjs && node scripts/notify-hook-route-test.cjs && node scripts/review-notify-test.cjs && node scripts/turn-idle-background-guard-test.cjs && node scripts/waiting-brief-test.cjs && node scripts/runtime-death-alert-test.cjs && node scripts/github-per-member-test.cjs && node scripts/github-multi-org-test.cjs && node scripts/card-measurement-test.cjs && node scripts/scheduler-admission-test.cjs && node scripts/tick-liveness-test.cjs && node scripts/audit-mirror-test.cjs && node scripts/request-metrics-test.cjs && node scripts/tool-usage-test.cjs && node scripts/sessions-list-perf-test.cjs && node scripts/summarizer-degradation-test.cjs && node scripts/agent-history-scope-test.cjs && node scripts/webhook-ingress-test.cjs && node scripts/slack-content-filter-test.cjs && node scripts/slack-ingress-test.cjs && node scripts/discord-ingress-test.cjs && node scripts/chat-attachments-test.cjs && node scripts/clickup-task-bridge-test.cjs && node scripts/agentric-commands-test.cjs && node scripts/whats-new-test.cjs && node scripts/opencode-gate-test.cjs && node scripts/protected-path-guard-test.cjs && node scripts/attach-grace-test.cjs && node scripts/attach-file-liveness-test.cjs && node scripts/feed-smoke.cjs && node scripts/activity-classify-test.cjs && node scripts/goal-room-test.cjs && node scripts/secret-rotation-test.cjs && node scripts/update-watch-test.cjs && node scripts/runtime-update-watch-test.cjs && node scripts/setup-wizard-test.cjs && node scripts/md-pdf-test.cjs && node scripts/proposal-surfacing-test.cjs && node scripts/process-janitor-test.cjs && node scripts/detached-work-steer-test.cjs && node scripts/statusline-install-test.cjs && node scripts/docs-create-agent-test.cjs && node scripts/agent-stats-rollup-test.cjs && node scripts/task-discussion-rollup-test.cjs && node scripts/session-insights-stamp-test.cjs && node scripts/loop-stall-attribution-test.cjs && node scripts/session-progress-test.cjs && node scripts/drift-nudge-test.cjs && node scripts/goal-metric-review-test.cjs && node scripts/capability-gap-test.cjs && node scripts/workflow-proposal-test.cjs && node scripts/automation-edit-proposal-test.cjs && node scripts/app-card-close-test.cjs",
"test:alert-staleness": "node scripts/alert-staleness-test.cjs",
"test:deps": "node scripts/deps-freshness-test.cjs && node scripts/runtime-account-test.cjs && node scripts/runtime-account-misattribution-test.cjs && node scripts/runtime-login-test.cjs && node scripts/claude-config-seed-test.cjs && node scripts/claude-config-isolation-test.cjs",
"test:dm-continuity": "node scripts/dm-continuity-test.cjs",
Expand Down
125 changes: 125 additions & 0 deletions scripts/cron-supersede-test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env node
/* Cron supersede test — an INTERACTIVE cron automation keeps its pane after the run finishes, and the
* scheduler's pile-up guard used to read that idle pane as "still running" and skip every later occurrence
* until someone closed the tab. Now the next occurrence closes the previous run when nobody is using it,
* and skips (audited once per occurrence) only when it really is in use: attached, claimed, mid-turn,
* blocked on a person, an unattended run, or attachment can't be polled. Isolated home; the session backend
* and createSession are stubbed so no tmux/claude runs. */
const fs = require('fs');
const os = require('os');
const path = require('path');

const ROOT = path.resolve(__dirname, '..');
const HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'aos-supersede-test-'));
process.env.AGENT_OS_HOME = HOME;
process.env.AGENT_OS_TENANT = 'testco';
process.env.AOS_NO_TTYD = '1';
delete process.env.AGENT_OS_SECRET_KEY;
delete process.env.AOS_MAX_CONCURRENT_SESSIONS;

let pass = 0, fail = 0;
const assert = (c, name, d) => c ? (pass++, console.log(` \x1b[32m✓\x1b[0m ${name}`)) : (fail++, console.log(` \x1b[31m✗ ${name}\x1b[0m${d ? ' — ' + d : ''}`));

const { loadAgentOS } = require(path.join(ROOT, 'dist/kernel.js'));
const { TerminalManager } = require(path.join(ROOT, 'dist/terminal.js'));
const { Automations } = require(path.join(ROOT, 'dist/edge/automations.js'));

const aos = loadAgentOS();
const tm = new TerminalManager(aos, 'http://127.0.0.1:0', path.join(HOME, 'tmux.sock'));
const autos = new Automations(aos, tm);

// ── stubs: a pane set stands in for tmux; createSession inserts a row instead of spawning ──
const panes = new Set();
const attached = new Map(); // tmux → true | false | null (null = can't poll)
const killed = [];
tm.backend.aliveNames = () => new Set(panes);
tm.backend.hasClient = (_s, t) => (attached.has(t) ? attached.get(t) : false);
tm.backend.kill = (_s, t) => { killed.push(t); panes.delete(t); };
let seq = 0;
tm.createSession = (agent, title, task, spawnedBy, headless) => {
const id = `s${++seq}`, tmux = `aos-${id}`, now = Date.now();
aos.db.prepare("INSERT INTO term_sessions (id,agent,title,task,tmux,status,headless,resident,spawned_by,created_at,updated_at) VALUES (?,?,?,?,?,'running',?,0,?,?,?)")
.run(id, agent, title, task, tmux, headless ? 1 : 0, spawnedBy, now, now);
panes.add(tmux);
return { id, tmux };
};

const AID = 'au_weekly';
aos.db.prepare("INSERT INTO automations (id, agent_id, name, type, mode, schedule, task, enabled, created_at) VALUES (?,?,?,?,?,?,?,?,?)")
.run(AID, 'seo', 'Weekly SEO loop', 'cron', 'interactive', '30 3 * * 1', 'Run the loop.', 1, Date.now() - 30 * 86400000);
const auto = () => autos.list().find((a) => a.id === AID);
const row = (id) => aos.db.prepare('SELECT * FROM term_sessions WHERE id = ?').get(id);
const audits = (type) => aos.db.prepare('SELECT data FROM audit_events WHERE type = ?').all(type).map((r) => JSON.parse(r.data));

// A finished-but-open previous run: turn started, then a turn-end landed after it.
const prevRun = (patch = {}) => {
const first = autos.fire(auto(), { guard: false });
const t = Date.now() - 3600_000;
aos.db.prepare('UPDATE term_sessions SET busy_since = ?, last_activity = ? WHERE id = ?').run(t, t + 60_000, first.sessionId);
for (const [k, v] of Object.entries(patch)) aos.db.prepare(`UPDATE term_sessions SET ${k} = ? WHERE id = ?`).run(v, first.sessionId);
return first.sessionId;
};

console.log('\n\x1b[1m1) idle, detached previous run → superseded, the cron fires\x1b[0m');
{
const prev = prevRun();
const r = autos.fire(auto(), { guard: true });
assert(r.ok === true, 'guarded fire succeeds', JSON.stringify(r));
assert(killed.includes(`aos-${prev}`), 'previous pane killed');
assert(row(prev).status === 'done', `previous row ended (status ${row(prev).status})`);
assert(auto().lastSessionId === r.sessionId, 'automation now points at the new run');
assert(audits('automation.superseded').some((d) => d.previous === prev), 'automation.superseded audited');
assert(aos.db.prepare("SELECT 1 FROM audit_events WHERE type='session.reaped' AND run_id=? AND data LIKE '%superseded%'").get(prev), 'session.reaped reason=superseded');
}

const skipCase = (name, setup, want) => {
const prev = prevRun(setup.patch);
setup.after && setup.after(prev);
const before = killed.length;
const r = autos.fire(auto(), { guard: true });
assert(r.ok === false && r.reason.includes(`(${want})`), `${name} → skipped (${want})`, JSON.stringify(r));
assert(killed.length === before && panes.has(`aos-${prev}`), `${name} → previous pane untouched`);
panes.delete(`aos-${prev}`); // clear the stage for the next case
};

console.log('\n\x1b[1m2) in use → keeps the skip\x1b[0m');
skipCase('attached', { after: (id) => attached.set(`aos-${id}`, true) }, 'attached');
skipCase('attachment unknown (null)', { after: (id) => attached.set(`aos-${id}`, null) }, 'attached');
skipCase('claimed', { patch: { claimed_by: 'm1' } }, 'claimed');
skipCase('mid-turn', { patch: { busy_since: Date.now() - 60_000, last_activity: Date.now() - 120_000 } }, 'working');
skipCase('blocked on a question', {
after: (id) => aos.db.prepare("INSERT INTO questions (id,run_id,tenant,agent,prompt,status,created_at) VALUES (?,?,?,'seo','RFQ?','pending',?)").run(`q_${id}`, id, 'testco', Date.now()),
}, 'blocked');
skipCase('unattended (headless) run still live', { patch: { headless: 1 } }, 'unattended');

console.log('\n\x1b[1m3) previous pane already gone → fires without a supersede\x1b[0m');
{
const prev = prevRun();
panes.delete(`aos-${prev}`);
const n = audits('automation.superseded').length;
const r = autos.fire(auto(), { guard: true });
assert(r.ok === true, 'fires');
assert(audits('automation.superseded').length === n, 'no supersede audited');
}

console.log('\n\x1b[1m4) tick() audits a skip once per occurrence, not once per tick\x1b[0m');
{
const prev = prevRun();
attached.set(`aos-${prev}`, true);
const monday = new Date(2026, 9, 5, 3, 30, 0, 0).getTime(); // a Monday 03:30 local
aos.db.prepare('UPDATE automations SET last_fired_at = ? WHERE id = ?').run(monday - 7 * 86400000, AID);
const n = audits('automation.skipped').length;
autos.tick(new Date(monday));
autos.tick(new Date(monday + 60_000));
autos.tick(new Date(monday + 120_000));
const skips = audits('automation.skipped').slice(n);
assert(skips.length === 1, `one automation.skipped for three ticks (got ${skips.length})`);
assert(skips[0] && skips[0].reason.includes('attached'), 'skip reason names why');
attached.set(`aos-${prev}`, false);
autos.tick(new Date(monday + 180_000));
assert(auto().lastSessionId !== prev, 'once detached, the next tick inside the catch-up window supersedes and fires');
}

console.log(`\n${fail === 0 ? '\x1b[32m' : '\x1b[31m'}CRON SUPERSEDE: ${pass}/${pass + fail} passed\x1b[0m`);
try { fs.rmSync(HOME, { recursive: true, force: true }); } catch {}
process.exit(fail === 0 ? 0 : 1);
Loading
Loading