atris 3.71.2 → 3.72.0
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/atris/skills/campaigns/SKILL.md +1 -1
- package/bin/atris.js +8 -2
- package/commands/autoland.js +102 -2
- package/commands/campaign.js +263 -11
- package/commands/clean.js +60 -4
- package/commands/gm.js +1273 -7
- package/commands/improve.js +3 -2
- package/commands/member.js +28 -3
- package/commands/mission.js +82 -27
- package/commands/pulse.js +89 -5
- package/commands/sync.js +2 -2
- package/commands/task.js +49 -13
- package/commands/worktree.js +24 -2
- package/commands/zen.js +183 -0
- package/lib/auto-accept-certified.js +9 -4
- package/lib/campaign-cadence.js +53 -14
- package/lib/campaign-core.js +98 -17
- package/lib/campaign-keep.js +91 -0
- package/lib/campaign-lanes.js +11 -6
- package/lib/campaign-loop.js +173 -32
- package/lib/campaign-reading.js +14 -1
- package/lib/campaign-rsi.js +697 -0
- package/lib/campaign-run-lock.js +9 -1
- package/lib/campaign-schedule.js +124 -0
- package/lib/campaign-schema.js +3 -1
- package/lib/campaign-store.js +34 -2
- package/lib/campaign-trace.js +77 -29
- package/lib/engine-resource.js +5 -1
- package/lib/gm-bet.js +2219 -0
- package/lib/gm-phone-page.js +1102 -0
- package/lib/gm-phone.js +682 -0
- package/lib/gm-season.js +1162 -0
- package/lib/gm-target.js +292 -0
- package/lib/job-code-verify.js +169 -0
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +105 -14
- package/lib/member-progress.js +2 -2
- package/lib/member-report.js +11 -1
- package/lib/member-self-next-job.js +6 -1
- package/lib/member-up.js +17 -8
- package/lib/merged-review-close.js +206 -0
- package/lib/mission-code-provenance.js +118 -0
- package/lib/mission-protected-lane.js +52 -4
- package/lib/pulse.js +2 -1
- package/lib/qr.js +338 -0
- package/lib/rsi-record.js +88 -0
- package/lib/task-receipt.js +24 -6
- package/lib/yes-lock.js +530 -0
- package/lib/zen-apply.js +511 -0
- package/lib/zen-workspace.js +121 -0
- package/lib/zen.js +395 -0
- package/package.json +1 -1
package/lib/campaign-run-lock.js
CHANGED
|
@@ -56,4 +56,12 @@ function acquireRunLock(home, campaignId, { pid = process.pid, host = os.hostnam
|
|
|
56
56
|
return { ok: false, holder: readHolder(file) };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// Who drives this campaign right now, or null. Same rules as acquireRunLock, nothing written.
|
|
60
|
+
function liveHolder(home, campaignId, { host = os.hostname(), isAlive = alive } = {}) {
|
|
61
|
+
const holder = readHolder(lockPath(home, campaignId));
|
|
62
|
+
if (!holder) return null;
|
|
63
|
+
if (holder.host !== host) return holder;
|
|
64
|
+
return isAlive(holder.pid) ? holder : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { acquireRunLock, liveHolder, lockPath };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// atris campaign schedule: a person says yes once, and the campaign starts on
|
|
2
|
+
// its own at set times until its budget is spent. The schedule lives beside the
|
|
3
|
+
// campaign's other files, not in its events, so other surfaces fold unchanged.
|
|
4
|
+
// `atris campaign wake` (the keep timer) and `atris campaign tick` fire it.
|
|
5
|
+
// Spec: atris/features/campaign/schedule.md.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const MIN = 60 * 1000;
|
|
13
|
+
const DAY_NAMES = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
14
|
+
const DAY_SETS = { daily: [0, 1, 2, 3, 4, 5, 6], weekdays: [1, 2, 3, 4, 5], weekends: [0, 6] };
|
|
15
|
+
const DEFAULT_STEPS = 4;
|
|
16
|
+
|
|
17
|
+
const HOW = 'say when like "weekdays 09:00", "daily 18:30", "mon,thu 07:00", or "30m" / "2h"';
|
|
18
|
+
|
|
19
|
+
// "30m", "2h", "daily 09:00", "weekdays 09:00", "mon,wed 14:30", or "09:00"
|
|
20
|
+
// (every day). Clock times are local to the computer that runs the timer.
|
|
21
|
+
function parseEvery(text) {
|
|
22
|
+
const value = String(text || '').trim().toLowerCase();
|
|
23
|
+
if (!value) return { error: `a schedule needs a time: ${HOW}` };
|
|
24
|
+
const interval = /^(\d+)\s*(m|min|mins|minutes?|h|hr|hrs|hours?)$/.exec(value);
|
|
25
|
+
if (interval) {
|
|
26
|
+
const ms = Number(interval[1]) * (interval[2].startsWith('h') ? 60 : 1) * MIN;
|
|
27
|
+
if (ms < MIN) return { error: 'a schedule runs at most once a minute' };
|
|
28
|
+
return { rule: { kind: 'interval', ms }, every: value };
|
|
29
|
+
}
|
|
30
|
+
const parts = value.split(/\s+/);
|
|
31
|
+
const clock = /^(\d{1,2}):(\d{2})$/.exec(parts[parts.length - 1]);
|
|
32
|
+
if (!clock || parts.length > 2) return { error: `cannot read "${text}": ${HOW}` };
|
|
33
|
+
const hour = Number(clock[1]);
|
|
34
|
+
const minute = Number(clock[2]);
|
|
35
|
+
if (hour > 23 || minute > 59) return { error: `"${parts[parts.length - 1]}" is not a time of day` };
|
|
36
|
+
let days = DAY_SETS.daily;
|
|
37
|
+
if (parts.length === 2) {
|
|
38
|
+
days = DAY_SETS[parts[0]];
|
|
39
|
+
if (!days) {
|
|
40
|
+
days = [];
|
|
41
|
+
for (const name of parts[0].split(',')) {
|
|
42
|
+
const day = DAY_NAMES.indexOf(name.slice(0, 3));
|
|
43
|
+
if (day < 0 || (name.length > 3 && !name.startsWith(DAY_NAMES[day]))) return { error: `"${name}" is not a day: ${HOW}` };
|
|
44
|
+
if (!days.includes(day)) days.push(day);
|
|
45
|
+
}
|
|
46
|
+
days.sort();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { rule: { kind: 'clock', days, hour, minute }, every: value };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The next slot after `after` (epoch ms), on a whole minute so it is due at the
|
|
53
|
+
// minute it is shown, and the one-minute timer never runs it a minute late.
|
|
54
|
+
function nextAt(rule, after) {
|
|
55
|
+
if (rule.kind === 'interval') return Math.floor((after + rule.ms) / MIN) * MIN;
|
|
56
|
+
const at = new Date(after);
|
|
57
|
+
at.setSeconds(0, 0);
|
|
58
|
+
at.setHours(rule.hour, rule.minute);
|
|
59
|
+
for (let i = 0; i < 8; i++) {
|
|
60
|
+
if (at.getTime() > after && rule.days.includes(at.getDay())) return at.getTime();
|
|
61
|
+
at.setDate(at.getDate() + 1);
|
|
62
|
+
at.setHours(rule.hour, rule.minute, 0, 0);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scheduleFile(home, campaignId) {
|
|
68
|
+
return path.join(home, 'campaigns', campaignId, 'schedule.json');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readSchedule(home, campaignId) {
|
|
72
|
+
if (!home) return null;
|
|
73
|
+
try {
|
|
74
|
+
const saved = JSON.parse(fs.readFileSync(scheduleFile(home, campaignId), 'utf8'));
|
|
75
|
+
return saved && saved.rule && Number.isFinite(saved.next_at) ? saved : null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writeSchedule(home, campaignId, schedule) {
|
|
82
|
+
const file = scheduleFile(home, campaignId);
|
|
83
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
84
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
85
|
+
fs.writeFileSync(tmp, `${JSON.stringify(schedule, null, 2)}\n`);
|
|
86
|
+
fs.renameSync(tmp, file);
|
|
87
|
+
return schedule;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function saveSchedule(home, campaignId, { every, rule, steps = DEFAULT_STEPS, by, now }) {
|
|
91
|
+
return writeSchedule(home, campaignId, {
|
|
92
|
+
campaign_id: campaignId, every, rule, steps, approved_by: by,
|
|
93
|
+
approved_at: new Date(now).toISOString(), last_fired_at: null, next_at: nextAt(rule, now),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function removeSchedule(home, campaignId) {
|
|
98
|
+
const file = scheduleFile(home, campaignId);
|
|
99
|
+
if (!fs.existsSync(file)) return false;
|
|
100
|
+
fs.unlinkSync(file);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isDue(schedule, now) {
|
|
105
|
+
return Boolean(schedule) && now >= schedule.next_at;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Use up the slot before the run starts, so a run that crashes does not fire
|
|
109
|
+
// again every minute. A slot missed while the computer slept fires once.
|
|
110
|
+
function markFired(home, campaignId, schedule, now) {
|
|
111
|
+
return writeSchedule(home, campaignId, { ...schedule, last_fired_at: new Date(now).toISOString(), next_at: nextAt(schedule.rule, now) });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function whenText(ms) {
|
|
115
|
+
const at = new Date(ms);
|
|
116
|
+
const day = at.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
117
|
+
return `${day} ${String(at.getHours()).padStart(2, '0')}:${String(at.getMinutes()).padStart(2, '0')}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function describe(schedule) {
|
|
121
|
+
return `${schedule.every}, up to ${schedule.steps} step${schedule.steps === 1 ? '' : 's'} each time; next ${whenText(schedule.next_at)}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { DEFAULT_STEPS, describe, isDue, markFired, nextAt, parseEvery, readSchedule, removeSchedule, saveSchedule, scheduleFile, whenText };
|
package/lib/campaign-schema.js
CHANGED
|
@@ -32,7 +32,9 @@ const STATES = Object.freeze([
|
|
|
32
32
|
|
|
33
33
|
const STOP_REASONS = Object.freeze(['judge_passed', 'stopped', 'done_by_human', 'budget_spent']);
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// A fault is a job that never ran (engine missing, crash before the first
|
|
36
|
+
// step): the setup broke, not the work.
|
|
37
|
+
const MISSION_OUTCOMES = Object.freeze(['kept', 'reverted', 'failed', 'fault']);
|
|
36
38
|
|
|
37
39
|
// What a person or agent writes. Everything else is folded from events.
|
|
38
40
|
const CAMPAIGN_FIELDS = Object.freeze({
|
package/lib/campaign-store.js
CHANGED
|
@@ -267,6 +267,20 @@ function createMemoryStore(options = {}) {
|
|
|
267
267
|
const missions = new Map();
|
|
268
268
|
const seqByOrigin = new Map();
|
|
269
269
|
const meta = new Map();
|
|
270
|
+
const eventIds = new Set();
|
|
271
|
+
const firstByOriginSeq = new Map();
|
|
272
|
+
let lookupIndexed = true;
|
|
273
|
+
const { isProxy } = require('node:util').types;
|
|
274
|
+
const indexable = (event) => {
|
|
275
|
+
// Reflection must not add caller Proxy traps or evaluate any getters.
|
|
276
|
+
if (isProxy(event) || event === null || typeof event !== 'object' || Object.getPrototypeOf(event) !== Object.prototype) return false;
|
|
277
|
+
const id = Object.getOwnPropertyDescriptor(event, 'id');
|
|
278
|
+
const origin = Object.getOwnPropertyDescriptor(event, 'origin');
|
|
279
|
+
const seq = Object.getOwnPropertyDescriptor(event, 'seq');
|
|
280
|
+
return id && Object.hasOwn(id, 'value') && typeof id.value === 'string'
|
|
281
|
+
&& origin && Object.hasOwn(origin, 'value') && typeof origin.value === 'string'
|
|
282
|
+
&& seq && Object.hasOwn(seq, 'value') && Number.isInteger(seq.value) && seq.value >= 1;
|
|
283
|
+
};
|
|
270
284
|
|
|
271
285
|
const clone = (value) => JSON.parse(JSON.stringify(value));
|
|
272
286
|
const count = (campaignId) => events.filter((e) => e.campaign_id === campaignId).length;
|
|
@@ -283,6 +297,18 @@ function createMemoryStore(options = {}) {
|
|
|
283
297
|
if (missionId && !missions.has(missionId)) missions.set(missionId, event.campaign_id);
|
|
284
298
|
seqByOrigin.set(event.origin, Math.max(seqByOrigin.get(event.origin) || 0, event.seq));
|
|
285
299
|
events.push(clone(event));
|
|
300
|
+
if (lookupIndexed) {
|
|
301
|
+
// Index the row actually stored, after any reentrant toJSON commits.
|
|
302
|
+
const stored = events[events.length - 1];
|
|
303
|
+
if (!indexable(stored)) {
|
|
304
|
+
lookupIndexed = false;
|
|
305
|
+
} else {
|
|
306
|
+
eventIds.add(stored.id);
|
|
307
|
+
let sequences = firstByOriginSeq.get(stored.origin);
|
|
308
|
+
if (!sequences) firstByOriginSeq.set(stored.origin, sequences = new Map());
|
|
309
|
+
if (!sequences.has(stored.seq)) sequences.set(stored.seq, stored);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
286
312
|
};
|
|
287
313
|
|
|
288
314
|
return {
|
|
@@ -306,8 +332,14 @@ function createMemoryStore(options = {}) {
|
|
|
306
332
|
ingest(event) {
|
|
307
333
|
const check = validateEvent(event);
|
|
308
334
|
if (!check.ok) throw storeError('invalid_event', `invalid ${event && event.type}: ${check.errors.join(', ')}`);
|
|
309
|
-
|
|
310
|
-
|
|
335
|
+
let clash;
|
|
336
|
+
if (lookupIndexed && indexable(event)) {
|
|
337
|
+
if (eventIds.has(event.id)) return false;
|
|
338
|
+
clash = firstByOriginSeq.get(event.origin)?.get(event.seq);
|
|
339
|
+
} else {
|
|
340
|
+
if (events.some((e) => e.id === event.id)) return false;
|
|
341
|
+
clash = events.find((e) => e.origin === event.origin && e.seq === event.seq);
|
|
342
|
+
}
|
|
311
343
|
if (clash) throw storeError('seq_conflict', `origin ${event.origin} seq ${event.seq} is already ${clash.id}`);
|
|
312
344
|
checkMission(event);
|
|
313
345
|
if (event.type === 'campaign_started' && !campaigns.has(event.campaign_id)) campaigns.set(event.campaign_id, event.at);
|
package/lib/campaign-trace.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
|
-
const { compareEvents, fold } = require('./campaign-core');
|
|
10
|
+
const { compareEvents, fold, foldWithSummaryPrefixes } = require('./campaign-core');
|
|
11
11
|
const { judgeReading } = require('./campaign-reading');
|
|
12
12
|
|
|
13
13
|
const SCHEMA = 'atris.campaign.trace';
|
|
@@ -18,7 +18,7 @@ const PRIVACY = {
|
|
|
18
18
|
full: 'Full: includes titles, objectives, verify commands, the judge command, judge summaries and output tails, and ask text. Export this only for your own data, or with the written consent of everyone in the campaign.',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
const REWARD = 'reward = reading_after - reading_before, where a reading is the judge score in [0, 1] from the judge runs on either side of the mission. Missions that ended between the same two judge runs form a batch
|
|
21
|
+
const REWARD = 'reward = reading_after - reading_before, where a reading is the judge score in [0, 1] from the judge runs on either side of the mission. An accepted judge reset between those runs makes the comparison unknown. Missions that ended between the same two judge runs form a batch; eligible missions split the change equally. Setup faults never ran, so they have no work reward and take no share of that change.';
|
|
22
22
|
|
|
23
23
|
function round(value, places = 6) {
|
|
24
24
|
const scale = 10 ** places;
|
|
@@ -55,16 +55,14 @@ function foldOrder(events) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function compactState(view, run) {
|
|
58
|
-
const missions = { proposed: 0, claimed: 0, running: 0, ended: 0, vetoed: 0, dropped: 0 };
|
|
59
|
-
for (const m of view.missions) missions[m.status] = (missions[m.status] || 0) + 1;
|
|
60
58
|
return {
|
|
61
59
|
state: view.state,
|
|
62
60
|
reading: readingOf(run).value,
|
|
63
|
-
confidence: view.
|
|
61
|
+
confidence: view.confidence,
|
|
64
62
|
judge_runs: view.judge_runs,
|
|
65
63
|
budget: view.budget,
|
|
66
|
-
missions,
|
|
67
|
-
open_asks: view.
|
|
64
|
+
missions: view.mission_counts,
|
|
65
|
+
open_asks: view.open_asks,
|
|
68
66
|
};
|
|
69
67
|
}
|
|
70
68
|
|
|
@@ -84,14 +82,20 @@ function buildTrace(events, options = {}) {
|
|
|
84
82
|
|
|
85
83
|
const missions = new Map();
|
|
86
84
|
const judgeRuns = [];
|
|
85
|
+
const judgeChanges = [];
|
|
86
|
+
// Use the fold's authorization decision rather than a second owner rule.
|
|
87
|
+
const ignoredJudgeChanges = new Set(final.warnings
|
|
88
|
+
.filter(warning => warning.code === 'judge_set_not_owner')
|
|
89
|
+
.map(warning => warning.event_id));
|
|
87
90
|
const asks = new Map();
|
|
88
91
|
const info = (id) => {
|
|
89
|
-
if (!missions.has(id)) missions.set(id, { id, firstPos: null, endPos: null, objective: null, verify: null, member: null, engine: null, kind: 'mission', estimate: null, ticks: 0, uncostedTicks: 0, endData: null, asks: [] });
|
|
92
|
+
if (!missions.has(id)) missions.set(id, { id, firstPos: null, endPos: null, objective: null, verify: null, member: null, engine: null, executionEngines: [], kind: 'mission', estimate: null, ticks: 0, uncostedTicks: 0, endData: null, asks: [] });
|
|
90
93
|
return missions.get(id);
|
|
91
94
|
};
|
|
92
95
|
|
|
93
96
|
sequence.forEach((event, pos) => {
|
|
94
97
|
const d = event.data;
|
|
98
|
+
if (event.type === 'judge_set' && !ignoredJudgeChanges.has(event.id)) judgeChanges.push(pos);
|
|
95
99
|
if (event.type === 'judge_ran') {
|
|
96
100
|
judgeRuns.push({ pos, at: event.at, passed: d.passed, reading: d.reading, summary: d.summary || null, outputTail: d.output_tail || null });
|
|
97
101
|
return;
|
|
@@ -117,8 +121,10 @@ function buildTrace(events, options = {}) {
|
|
|
117
121
|
if (typeof d.engine === 'string' && event.type === 'mission_started' && d.kind === 'plan') m.engine = d.engine;
|
|
118
122
|
if (typeof d.estimate_usd === 'number' && m.estimate === null) m.estimate = d.estimate_usd;
|
|
119
123
|
if (event.type === 'mission_started' && d.kind === 'plan') m.kind = 'plan';
|
|
124
|
+
if (event.type === 'mission_started' && !m.startedAt) m.startedAt = event.at;
|
|
120
125
|
if (event.type === 'step') {
|
|
121
126
|
m.ticks += 1;
|
|
127
|
+
if (d.kind !== 'plan') m.executionEngines.push(typeof d.engine === 'string' && d.engine ? d.engine : null);
|
|
122
128
|
if (!(typeof d.cost_usd === 'number' && Number.isFinite(d.cost_usd) && d.cost_usd >= 0)) m.uncostedTicks += 1;
|
|
123
129
|
}
|
|
124
130
|
if (event.type === 'mission_ended' && m.endPos === null) {
|
|
@@ -129,24 +135,53 @@ function buildTrace(events, options = {}) {
|
|
|
129
135
|
});
|
|
130
136
|
for (const ask of asks.values()) if (ask.mission_id && missions.has(ask.mission_id)) missions.get(ask.mission_id).asks.push(ask);
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
const ended = [...missions.values()].filter((m) => m.endPos !== null).sort((a, b) => a.endPos - b.endPos);
|
|
139
|
+
// Reuse one event traversal and retain only the compact state needed for
|
|
140
|
+
// ended missions. Reading lookup below still uses the original position.
|
|
141
|
+
const prefixStates = ended.length
|
|
142
|
+
? foldWithSummaryPrefixes(sequence, ended.map((m) => Math.max(1, m.firstPos === null ? m.endPos : m.firstPos))).prefixes
|
|
143
|
+
: new Map();
|
|
138
144
|
const lastRunBefore = (pos) => {
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
let lo = 0;
|
|
146
|
+
let hi = judgeRuns.length;
|
|
147
|
+
while (lo < hi) {
|
|
148
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
149
|
+
if (judgeRuns[mid].pos < pos) lo = mid + 1;
|
|
150
|
+
else hi = mid;
|
|
151
|
+
}
|
|
152
|
+
return judgeRuns[lo - 1] || null;
|
|
153
|
+
};
|
|
154
|
+
const firstRunAfter = (pos) => {
|
|
155
|
+
let lo = 0;
|
|
156
|
+
let hi = judgeRuns.length;
|
|
157
|
+
while (lo < hi) {
|
|
158
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
159
|
+
if (judgeRuns[mid].pos <= pos) lo = mid + 1;
|
|
160
|
+
else hi = mid;
|
|
161
|
+
}
|
|
162
|
+
return judgeRuns[lo] || null;
|
|
163
|
+
};
|
|
164
|
+
const judgeChangedBetween = (beforePos, afterPos) => {
|
|
165
|
+
let lo = 0;
|
|
166
|
+
let hi = judgeChanges.length;
|
|
167
|
+
while (lo < hi) {
|
|
168
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
169
|
+
if (judgeChanges[mid] <= beforePos) lo = mid + 1;
|
|
170
|
+
else hi = mid;
|
|
171
|
+
}
|
|
172
|
+
return lo < judgeChanges.length && judgeChanges[lo] < afterPos;
|
|
142
173
|
};
|
|
143
|
-
const firstRunAfter = (pos) => judgeRuns.find((run) => run.pos > pos) || null;
|
|
144
174
|
|
|
145
175
|
// Where the campaign finished, if it did: the first position whose prefix is done.
|
|
146
176
|
let finishPos = null;
|
|
147
177
|
if (final.state === 'done') {
|
|
148
178
|
let lo = 0;
|
|
149
179
|
let hi = sequence.length - 1;
|
|
180
|
+
for (const [count, snapshot] of prefixStates) {
|
|
181
|
+
if (snapshot.state === 'done') { hi = count - 1; break; }
|
|
182
|
+
lo = count;
|
|
183
|
+
}
|
|
184
|
+
|
|
150
185
|
while (lo < hi) {
|
|
151
186
|
const mid = Math.floor((lo + hi) / 2);
|
|
152
187
|
if (fold(sequence.slice(0, mid + 1)).state === 'done') hi = mid;
|
|
@@ -156,7 +191,7 @@ function buildTrace(events, options = {}) {
|
|
|
156
191
|
}
|
|
157
192
|
|
|
158
193
|
const finalMissions = new Map(final.missions.map((m) => [m.id, m]));
|
|
159
|
-
const
|
|
194
|
+
const outcomeOf = (m) => (finalMissions.get(m.id) || {}).outcome || m.endData.outcome;
|
|
160
195
|
|
|
161
196
|
// A batch: missions that ended between the same two judge runs.
|
|
162
197
|
const batches = new Map();
|
|
@@ -164,8 +199,10 @@ function buildTrace(events, options = {}) {
|
|
|
164
199
|
const before = lastRunBefore(m.endPos);
|
|
165
200
|
const after = firstRunAfter(m.endPos);
|
|
166
201
|
const key = `${before ? before.pos : -1}:${after ? after.pos : 'open'}`;
|
|
167
|
-
if (!batches.has(key)) batches.set(key, { index: batches.size + 1, before, after, missions: [] });
|
|
168
|
-
batches.get(key)
|
|
202
|
+
if (!batches.has(key)) batches.set(key, { index: batches.size + 1, before, after, missions: [], eligible: 0 });
|
|
203
|
+
const batch = batches.get(key);
|
|
204
|
+
batch.missions.push(m);
|
|
205
|
+
if (outcomeOf(m) !== 'fault') batch.eligible += 1;
|
|
169
206
|
}
|
|
170
207
|
|
|
171
208
|
const memberFor = (member) => (redact && typeof member === 'string' && member.startsWith('human:') ? 'human' : member);
|
|
@@ -178,23 +215,31 @@ function buildTrace(events, options = {}) {
|
|
|
178
215
|
const after = firstRunAfter(m.endPos);
|
|
179
216
|
const batch = batches.get(`${before ? before.pos : -1}:${after ? after.pos : 'open'}`);
|
|
180
217
|
const size = batch.missions.length;
|
|
218
|
+
const outcome = outcomeOf(m);
|
|
219
|
+
const judgeChanged = before && after && judgeChangedBetween(before.pos, after.pos);
|
|
181
220
|
const readBefore = readingOf(before);
|
|
182
|
-
|
|
221
|
+
// A replacement baseline is real campaign state, but cannot score earlier
|
|
222
|
+
// work. Null also prevents a replacement pass becoming fallback RSI proof.
|
|
223
|
+
const readAfter = readingOf(judgeChanged ? null : after);
|
|
183
224
|
const delta = readBefore.value !== null && readAfter.value !== null ? readAfter.value - readBefore.value : null;
|
|
184
225
|
const startPos = m.firstPos === null ? m.endPos : m.firstPos;
|
|
185
|
-
const view = viewBefore(startPos);
|
|
186
226
|
const folded = finalMissions.get(m.id) || {};
|
|
187
227
|
const uncosted = typeof m.endData.uncosted_runs === 'number' ? m.endData.uncosted_runs : m.uncostedTicks;
|
|
188
228
|
let creditNote = null;
|
|
189
|
-
if (
|
|
229
|
+
if (outcome === 'fault') creditNote = 'this mission never ran (setup fault), so it has no work reward and takes no share of the reading change';
|
|
230
|
+
else if (!after) creditNote = 'no judge run after this mission yet, so it has no reward';
|
|
190
231
|
else if (!before) creditNote = 'no judge run before this mission, so it has no reward';
|
|
232
|
+
else if (judgeChanged) creditNote = 'the judge changed between readings, so this mission has no comparable reward';
|
|
233
|
+
else if (batch.eligible < size) creditNote = `one of ${batch.eligible} eligible missions in this batch; setup faults are excluded and the reading change is split equally`;
|
|
191
234
|
else if (size > 1) creditNote = `one of ${size} missions that ended between the same two judge runs; the reading change is split equally`;
|
|
192
235
|
|
|
193
236
|
const action = {
|
|
194
237
|
mission_id: idFor(m.id),
|
|
195
238
|
kind: m.kind,
|
|
196
239
|
member: memberFor(folded.member || m.member),
|
|
197
|
-
engine: m.
|
|
240
|
+
engine: m.executionEngines.length
|
|
241
|
+
? new Set(m.executionEngines).size === 1 ? m.executionEngines[0] : null
|
|
242
|
+
: m.engine || (typeof m.endData.engine === 'string' ? m.endData.engine : null),
|
|
198
243
|
estimate_usd: m.estimate,
|
|
199
244
|
};
|
|
200
245
|
if (!redact) {
|
|
@@ -207,9 +252,9 @@ function buildTrace(events, options = {}) {
|
|
|
207
252
|
step: i + 1,
|
|
208
253
|
at: m.endAt,
|
|
209
254
|
batch: { index: batch.index, size },
|
|
210
|
-
state_before: compactState(
|
|
255
|
+
state_before: compactState(prefixStates.get(Math.max(1, startPos)), lastRunBefore(startPos)),
|
|
211
256
|
action,
|
|
212
|
-
outcome
|
|
257
|
+
outcome,
|
|
213
258
|
cost_usd: folded.cost_usd === undefined || folded.cost_usd === null ? m.endData.cost_usd : folded.cost_usd,
|
|
214
259
|
ticks: m.ticks,
|
|
215
260
|
uncosted_ticks: uncosted,
|
|
@@ -217,12 +262,15 @@ function buildTrace(events, options = {}) {
|
|
|
217
262
|
reading_before: readBefore.value,
|
|
218
263
|
reading_after: readAfter.value,
|
|
219
264
|
reading_source: readAfter.source,
|
|
220
|
-
reward: delta === null ? null : round(delta /
|
|
265
|
+
reward: outcome === 'fault' || delta === null ? null : round(delta / batch.eligible),
|
|
221
266
|
credit_note: creditNote,
|
|
222
267
|
done: finishPos !== null && finishPos <= (after ? after.pos : lastPos),
|
|
223
|
-
judge_passed: after ? after.passed : null,
|
|
268
|
+
judge_passed: after && !judgeChanged ? after.passed : null,
|
|
224
269
|
};
|
|
225
270
|
if (!redact) {
|
|
271
|
+
row.started_at = m.startedAt || null;
|
|
272
|
+
row.receipt_path = typeof m.endData.receipt_path === 'string' ? m.endData.receipt_path : null;
|
|
273
|
+
row.execution_engines = m.executionEngines;
|
|
226
274
|
row.judge_summary = after ? after.summary : null;
|
|
227
275
|
row.judge_output_tail = after ? after.outputTail : null;
|
|
228
276
|
row.asks = m.asks.map((ask) => ({ question: ask.question, answer: ask.answer }));
|
package/lib/engine-resource.js
CHANGED
|
@@ -93,7 +93,11 @@ const SYSTEM_TOOLS = {
|
|
|
93
93
|
|
|
94
94
|
function defaultRun(cmd, args) {
|
|
95
95
|
const full = SYSTEM_TOOLS[cmd];
|
|
96
|
-
|
|
96
|
+
const options = { encoding: 'utf8', timeout: 3000 };
|
|
97
|
+
// Environment listings grow with local jobs; keep a finite allowance above
|
|
98
|
+
// Node's default 1 MiB without changing the deadline or failed-read policy.
|
|
99
|
+
if (cmd === 'ps') options.maxBuffer = 8 * 1024 * 1024;
|
|
100
|
+
return spawnSync(full && fs.existsSync(full) ? full : cmd, args, options);
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
function parseFreePct(text) {
|