mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Support audited expiring owner continuation allowances
This commit is contained in:
1 parent
c8b49e5019
commit
363a81d555
6 files changed
+94
-14
No files matched your search
@@ -1,5 +1,28 @@
|
||||
# Test Integrity Notes
|
||||
|
||||
## 2026-10-03 — Audited one-day additional allowance
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: new_tests
|
||||
scenarios:
|
||||
- id: SCN-runtime-continuation-day-override
|
||||
given: "An exhausted owner-approved daily allowance and retained charged attempts"
|
||||
when: "The owner approves a bounded current-UTC-day total and later pauses or the day rolls over"
|
||||
then: "Extra work starts without resetting attempts; non-owner/invalid overrides refuse; pause persists; the following day uses the original daily cap"
|
||||
observable_outcomes:
|
||||
- "Actual launched count, charged attempts, remaining allowance, owner audit and next-day limit"
|
||||
determinism_controls:
|
||||
- "Hermetic project, fixed clock and controlled launch callback; no live provider or host scheduling"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "Erasing usage or permanently removing daily caps on an owner override"
|
||||
- "Allowing a specialist to authorize extra budget or bypass pause"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
## 2026-10-03 — Owner-configured concurrency
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# One-day continuation allowance override
|
||||
|
||||
An explicit owner decision can grant a higher bounded total for the current UTC
|
||||
day using `torch continuation enable --today-max-turns 96 --yes`.
|
||||
The override total includes turns already charged; it never erases usage.
|
||||
The ordinary daily cap (1–48) remains unchanged. The override ceiling is 96.
|
||||
At UTC midnight the override ceases to apply without a scheduled reset write.
|
||||
Owner-only audited configuration, seven-slot concurrency, identity guards,
|
||||
pause, unknown-outcome protection and all verification boundaries remain intact.
|
||||
Omitting the override argument preserves its existing same-day decision.
|
||||
This is a turn allowance, not a measured token, subscription-credit or cost limit.
|
||||
|
||||
SCN-runtime-continuation-day-override checks actual extra dispatch, preserved
|
||||
charges, authorization/range refusals, pause and automatic next-day expiry.
|
||||
+1
-1
@@ -1041,7 +1041,7 @@ function renderAttention(groups) {
|
||||
function render(snapshot) {
|
||||
const continuation = snapshot.continuation;
|
||||
setText('#continuation-status', continuation?.available
|
||||
? `Automatic turns: ${continuation.attempts}/${continuation.maxTurnsPerDay} used today (UTC); ${continuation.remainingTurns} left. Concurrency policy: up to ${continuation.maxConcurrency ?? 3}. ${continuation.stopReason === 'daily-turn-cap'
|
||||
? `Automatic turns: ${continuation.attempts}/${continuation.maxTurnsPerDay} used today (UTC); ${continuation.remainingTurns} left. ${continuation.dailyOverride?.day === continuation.day ? `Owner-approved extra allowance expires at UTC midnight; normal daily cap ${continuation.configuredMaxTurnsPerDay}. ` : ''}Concurrency policy: up to ${continuation.maxConcurrency ?? 3}. ${continuation.stopReason === 'daily-turn-cap'
|
||||
? 'No further automatic starts: the owner-controlled daily cap is reached. Running turns may finish.'
|
||||
: continuation.stopReason === 'paused' ? 'Owner policy is paused; no new automatic starts.' : 'Policy permits new eligible turns; this does not verify timer installation.'} ${continuation.held?.length ?? 0} unchanged-work hold(s) need Manager coordination. This is not measured token or cost usage.`
|
||||
: 'Automatic continuation policy is unavailable; do not assume the fleet can start more work.');
|
||||
|
||||
+2
-1
@@ -98,7 +98,7 @@ Usage:
|
||||
torch setup [--parent <path>] [--dry-run] [--yes] [--json]
|
||||
torch up [--fresh] [--only <id,id>] [--dry-run] [--yes] [--json]
|
||||
torch recover-runtime --area <id> --unit <service> --invocation <id> [--expected-session <id> --expected-updated-at <time> --yes] [--json]
|
||||
torch continuation <status|enable|pause|tick> [--max-turns-per-day <1-48>] [--concurrency <1-7>] [--limit <1-7>] [--yes] [--json]
|
||||
torch continuation <status|enable|pause|tick> [--max-turns-per-day <1-48>] [--today-max-turns <1-96>] [--concurrency <1-7>] [--limit <1-7>] [--yes] [--json]
|
||||
torch down [--dry-run] [--yes] [--json]
|
||||
torch capture [--json]
|
||||
torch detach [--dry-run] [--yes] [--json]
|
||||
@@ -591,6 +591,7 @@ export async function runCli(argv = process.argv.slice(2), {
|
||||
if (operation === 'enable' || operation === 'pause') {
|
||||
print(configureContinuation(control, { actorId: 'owner', enabled: operation === 'enable',
|
||||
maxTurnsPerDay: optionValue(argv, '--max-turns-per-day') === undefined ? undefined : Number(optionValue(argv, '--max-turns-per-day')),
|
||||
todayMaxTurns: optionValue(argv, '--today-max-turns') === undefined ? undefined : Number(optionValue(argv, '--today-max-turns')),
|
||||
maxConcurrency: optionValue(argv, '--concurrency') === undefined ? undefined : Number(optionValue(argv, '--concurrency')) }), { json }); return 0;
|
||||
}
|
||||
if (operation !== 'tick') throw new TorchError('Unknown continuation operation.', { code: 'UNKNOWN_COMMAND' });
|
||||
|
||||
@@ -9,6 +9,9 @@ function occupiedAreas(control) {
|
||||
return existsSync(path) ? readdirSync(path, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name) : [];
|
||||
}
|
||||
function taskKey(tasks) { return JSON.stringify(tasks.map(t => [t.id, t.revision]).sort((a, b) => a[0].localeCompare(b[0]))); }
|
||||
function dailyCap(state, day) {
|
||||
return state.dailyOverride?.day === day ? Math.max(state.maxTurnsPerDay, state.dailyOverride.maxTurns) : state.maxTurnsPerDay;
|
||||
}
|
||||
function read(control) {
|
||||
const policyPath = join(control.stateRoot, 'continuation-policy.json');
|
||||
const state = { enabled: false, maxTurnsPerDay: 12, maxConcurrency: 3, areas: [], day: null, attempts: 0, handled: {}, progress: {},
|
||||
@@ -18,7 +21,9 @@ function read(control) {
|
||||
|| state.maxTurnsPerDay < 1 || state.maxTurnsPerDay > 48 || !Array.isArray(state.areas)
|
||||
|| !Number.isInteger(state.maxConcurrency) || state.maxConcurrency < 1 || state.maxConcurrency > 7
|
||||
|| !Number.isInteger(state.attempts) || state.attempts < 0 || !state.handled || typeof state.handled !== 'object'
|
||||
|| !state.progress || typeof state.progress !== 'object') {
|
||||
|| !state.progress || typeof state.progress !== 'object'
|
||||
|| (state.dailyOverride != null && (!/^\d{4}-\d{2}-\d{2}$/.test(state.dailyOverride.day)
|
||||
|| !Number.isInteger(state.dailyOverride.maxTurns) || state.dailyOverride.maxTurns < 1 || state.dailyOverride.maxTurns > 96))) {
|
||||
throw new TorchError('Continuation policy or attempt ledger is invalid; launch refused.', { code: 'CONTINUATION_POLICY_INVALID' });
|
||||
}
|
||||
return state;
|
||||
@@ -26,26 +31,28 @@ function read(control) {
|
||||
function save(control, state, policy = false) {
|
||||
const path = policy ? join(control.stateRoot, 'continuation-policy.json') : statePath(control);
|
||||
const temporary = `${path}.${randomUUID()}.tmp`;
|
||||
const value = policy ? { enabled: state.enabled, maxTurnsPerDay: state.maxTurnsPerDay, maxConcurrency: state.maxConcurrency, areas: state.areas }
|
||||
const value = policy ? { enabled: state.enabled, maxTurnsPerDay: state.maxTurnsPerDay, maxConcurrency: state.maxConcurrency, areas: state.areas, dailyOverride: state.dailyOverride ?? null }
|
||||
: { day: state.day, attempts: state.attempts, handled: state.handled, progress: state.progress };
|
||||
writeFileSync(temporary, JSON.stringify(value), { flag: 'wx', mode: 0o600 });
|
||||
renameSync(temporary, path);
|
||||
}
|
||||
|
||||
export function configureContinuation(control, { actorId, enabled, maxTurnsPerDay, maxConcurrency } = {}) {
|
||||
export function configureContinuation(control, { actorId, enabled, maxTurnsPerDay, maxConcurrency, todayMaxTurns } = {}) {
|
||||
control.assertOwnerActor(actorId);
|
||||
const previous = read(control);
|
||||
maxTurnsPerDay ??= previous.maxTurnsPerDay;
|
||||
maxConcurrency ??= previous.maxConcurrency;
|
||||
if (typeof enabled !== 'boolean' || !Number.isInteger(maxTurnsPerDay) || maxTurnsPerDay < 1 || maxTurnsPerDay > 48
|
||||
|| !Number.isInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 7) {
|
||||
throw new TorchError('Continuation requires explicit enable/pause, 1–48 daily turns and 1–7 concurrent turns.', { code: 'CONTINUATION_POLICY_INVALID' });
|
||||
|| !Number.isInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 7
|
||||
|| (todayMaxTurns !== undefined && (!Number.isInteger(todayMaxTurns) || todayMaxTurns < maxTurnsPerDay || todayMaxTurns > 96))) {
|
||||
throw new TorchError('Continuation requires explicit enable/pause, 1–48 daily turns, 1–7 concurrent turns and an optional same-day total between the daily cap and 96.', { code: 'CONTINUATION_POLICY_INVALID' });
|
||||
}
|
||||
const state = { ...previous, enabled, maxTurnsPerDay, maxConcurrency, areas: control.listAgents().map(a => a.areaId) };
|
||||
if (todayMaxTurns !== undefined) state.dailyOverride = { day: control.clock().toISOString().slice(0, 10), maxTurns: todayMaxTurns };
|
||||
save(control, state, true);
|
||||
control.auditOwnerAction({ actorId, operation: 'runtime.continuation.configure', entityType: 'project', entityId: control.projectId,
|
||||
details: { enabled, maxTurnsPerDay, maxConcurrency, areas: state.areas } });
|
||||
return { enabled, maxTurnsPerDay, maxConcurrency, areas: state.areas, sessionsStarted: false };
|
||||
details: { enabled, maxTurnsPerDay, maxConcurrency, dailyOverride: state.dailyOverride ?? null, areas: state.areas } });
|
||||
return { enabled, maxTurnsPerDay, maxConcurrency, dailyOverride: state.dailyOverride ?? null, areas: state.areas, sessionsStarted: false };
|
||||
}
|
||||
|
||||
export function observeContinuation({ stateRoot, tasks = [], now = () => new Date() } = {}) {
|
||||
@@ -53,13 +60,15 @@ export function observeContinuation({ stateRoot, tasks = [], now = () => new Dat
|
||||
const state = read({ stateRoot });
|
||||
const day = now().toISOString().slice(0, 10);
|
||||
const attempts = state.day === day ? state.attempts : 0;
|
||||
const remainingTurns = Math.max(0, state.maxTurnsPerDay - attempts);
|
||||
const cap = dailyCap(state, day);
|
||||
const remainingTurns = Math.max(0, cap - attempts);
|
||||
const held = Object.entries(state.progress).filter(([area, progress]) => {
|
||||
const active = tasks.filter(t => t.owner === area && ['assigned', 'in_progress'].includes(t.state));
|
||||
return active.length && progress?.status === 0 && progress.noProgress >= 2 && progress.taskKey === taskKey(active);
|
||||
}).map(([areaId]) => ({ areaId, reason: 'no-progress-needs-coordination' }));
|
||||
return { available: existsSync(join(stateRoot, 'continuation-policy.json')), enabled: state.enabled,
|
||||
day, dayTimezone: 'UTC', attempts, maxTurnsPerDay: state.maxTurnsPerDay, maxConcurrency: state.maxConcurrency, remainingTurns,
|
||||
day, dayTimezone: 'UTC', attempts, maxTurnsPerDay: cap, configuredMaxTurnsPerDay: state.maxTurnsPerDay,
|
||||
dailyOverride: state.dailyOverride ?? null, maxConcurrency: state.maxConcurrency, remainingTurns,
|
||||
stopReason: !state.enabled ? 'paused' : remainingTurns === 0 ? 'daily-turn-cap' : null,
|
||||
capacityHeldBy: occupiedAreas({ stateRoot }), held,
|
||||
budgetKind: 'automatic-turn-cap', manualTurnsIncluded: false, tokenOrCostGovernance: 'unknown',
|
||||
@@ -73,10 +82,11 @@ export function planContinuation(control, backlog, { at = new Date(), limit } =
|
||||
const state = read(control);
|
||||
const day = at.toISOString().slice(0, 10);
|
||||
const attempts = state.day === day ? state.attempts : 0;
|
||||
const cap = dailyCap(state, day);
|
||||
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 7)) throw new TorchError('Continuation limit must be 1–7.', { code: 'CONTINUATION_POLICY_INVALID' });
|
||||
const capacity = Math.min(limit ?? state.maxConcurrency, state.maxConcurrency);
|
||||
const metadata = JSON.parse(readFileSync(join(control.stateRoot, 'project.json'), 'utf8'));
|
||||
if (!state.enabled || metadata.detachedAt || attempts >= state.maxTurnsPerDay) {
|
||||
if (!state.enabled || metadata.detachedAt || attempts >= cap) {
|
||||
return { enabled: state.enabled, maxConcurrency: capacity, reason: metadata.detachedAt ? 'detached' : !state.enabled ? 'paused' : 'daily-turn-cap', candidates: [], attempts };
|
||||
}
|
||||
const tasks = backlog.list();
|
||||
@@ -107,9 +117,9 @@ export function planContinuation(control, backlog, { at = new Date(), limit } =
|
||||
const ordinal = area => state.handled[area]?.dispatchedDay === day ? state.handled[area].dispatchedOrdinal ?? 0 : 0;
|
||||
candidates.sort((a, b) => ordinal(a.areaId) - ordinal(b.areaId)
|
||||
|| Number(b.areaId === 'session-manager') - Number(a.areaId === 'session-manager'));
|
||||
return { enabled: true, maxConcurrency: capacity, day, attempts, remainingTurns: state.maxTurnsPerDay - attempts,
|
||||
return { enabled: true, maxConcurrency: capacity, day, attempts, remainingTurns: cap - attempts,
|
||||
occupiedAreas: occupiedAreas(control), held,
|
||||
candidates: candidates.slice(0, Math.min(capacity, state.maxTurnsPerDay - attempts)), mutationPerformed: false };
|
||||
candidates: candidates.slice(0, Math.min(capacity, cap - attempts)), mutationPerformed: false };
|
||||
}
|
||||
|
||||
export async function runContinuation(control, backlog, { actorId, launch, at, clock = () => at ?? new Date(), limit } = {}) {
|
||||
|
||||
@@ -219,6 +219,38 @@ test('SCN-runtime-continuation: explicit enable, event deduplication, daily cap
|
||||
} finally { f.control.close(); }
|
||||
});
|
||||
|
||||
test('SCN-runtime-continuation-day-override: owner extra allowance preserves charges and expires at UTC reset', async () => {
|
||||
const f = fixture();
|
||||
const backlog = { list: () => [] };
|
||||
const at = new Date('2026-10-03T00:00:00Z');
|
||||
try {
|
||||
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Ready' });
|
||||
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 1 });
|
||||
f.control.sendOwnerRequest({ actorId: 'owner', recipient: f.areaId, body: 'First work' });
|
||||
const run = () => runContinuation(f.control, backlog, { actorId: 'owner', at, launch: async () => 0 });
|
||||
assert.equal((await run()).launched.length, 1);
|
||||
assert.equal(planContinuation(f.control, backlog, { at }).reason, 'daily-turn-cap');
|
||||
assert.throws(() => configureContinuation(f.control, { actorId: f.areaId, enabled: true, todayMaxTurns: 3 }), { code: 'OWNER_AUTHORITY_REQUIRED' });
|
||||
for (const todayMaxTurns of [0, 97, 1.5]) assert.throws(() => configureContinuation(f.control,
|
||||
{ actorId: 'owner', enabled: true, todayMaxTurns }), { code: 'CONTINUATION_POLICY_INVALID' });
|
||||
configureContinuation(f.control, { actorId: 'owner', enabled: true, todayMaxTurns: 3 });
|
||||
assert.equal(planContinuation(f.control, backlog, { at }).attempts, 1, 'Previously charged turn is retained');
|
||||
assert.equal(planContinuation(f.control, backlog, { at }).remainingTurns, 2);
|
||||
f.control.sendOwnerRequest({ actorId: 'owner', recipient: f.areaId, body: 'Additional work' });
|
||||
assert.equal((await run()).launched.length, 1);
|
||||
configureContinuation(f.control, { actorId: 'owner', enabled: false });
|
||||
const sameDay = observeContinuation({ stateRoot: f.control.stateRoot, now: () => at });
|
||||
assert.equal(sameDay.attempts, 2);
|
||||
assert.equal(sameDay.maxTurnsPerDay, 3);
|
||||
assert.equal(sameDay.stopReason, 'paused');
|
||||
const nextDay = observeContinuation({ stateRoot: f.control.stateRoot, now: () => new Date('2026-10-04T00:00:00Z') });
|
||||
assert.equal(nextDay.maxTurnsPerDay, 1, 'Override cannot widen future daily allowances');
|
||||
assert.equal(nextDay.attempts, 0);
|
||||
assert.ok(f.control.readAudit({ limit: 100 }).some(a => a.operation === 'runtime.continuation.configure'
|
||||
&& a.details?.dailyOverride?.maxTurns === 3));
|
||||
} finally { f.control.close(); }
|
||||
});
|
||||
|
||||
test('SCN-runtime-continuation-concurrency: seven authorized starts preserve budget and obey tighter limits', async () => {
|
||||
const f = fixture({ workerCount: 5 });
|
||||
const backlog = { list: () => [] };
|
||||
|
||||
Reference in new issue
Block a user