bareloop 0.34.0 → 0.36.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/CHANGELOG.md +58 -0
- package/bareloop.context.md +276 -20
- package/package.json +1 -1
- package/src/authorrun.js +3 -2
- package/src/bundlerun.js +17 -12
- package/src/draftspend.js +197 -0
- package/src/exits.js +10 -1
- package/src/index.js +3 -3
- package/src/interviewrun.js +7 -7
- package/src/job.js +37 -0
- package/src/judged.js +2 -2
- package/src/kinds.js +2 -1
- package/src/ledger.js +101 -5
- package/src/legs.js +171 -0
- package/src/monthly.js +111 -72
- package/src/movefile.js +22 -0
- package/src/npminstall.js +55 -0
- package/src/panel/authorroutes.js +84 -14
- package/src/panel/authorsession.js +490 -125
- package/src/panel/importroutes.js +359 -0
- package/src/panel/importrun.js +203 -0
- package/src/panel/index.html +1194 -213
- package/src/panel/runroutes.js +219 -0
- package/src/panel/server.js +964 -79
- package/src/panel/settingsroutes.js +52 -2
- package/src/panel/status.js +51 -0
- package/src/planrun.js +60 -26
- package/src/providerrows.js +17 -0
- package/src/ralph.js +4 -1
- package/src/replay.js +139 -19
- package/src/replayio.js +11 -4
- package/src/reuse.js +20 -3
- package/src/run.js +6 -3
- package/src/runlist.js +75 -7
- package/src/source.js +43 -4
- package/src/text.js +20 -0
- package/src/u-patient.js +3 -2
- package/src/u-readout.js +9 -1
- package/src/u-watchdog.mjs +4 -2
- package/src/userrun.js +192 -65
- package/src/worktree.js +120 -0
- package/types/draftspend.d.ts +141 -0
- package/types/job.d.ts +25 -0
- package/types/ledger.d.ts +44 -0
- package/types/legs.d.ts +103 -0
- package/types/monthly.d.ts +23 -18
- package/types/movefile.d.ts +9 -0
- package/types/npminstall.d.ts +30 -0
- package/types/panel/authorroutes.d.ts +23 -1
- package/types/panel/authorsession.d.ts +110 -2
- package/types/panel/importroutes.d.ts +129 -0
- package/types/panel/importrun.d.ts +77 -0
- package/types/panel/runroutes.d.ts +32 -0
- package/types/panel/server.d.ts +266 -2
- package/types/panel/settingsroutes.d.ts +6 -1
- package/types/panel/status.d.ts +51 -0
- package/types/planrun.d.ts +13 -21
- package/types/providerrows.d.ts +12 -0
- package/types/ralph.d.ts +19 -0
- package/types/replay.d.ts +25 -1
- package/types/replayio.d.ts +2 -2
- package/types/reuse.d.ts +5 -0
- package/types/run.d.ts +6 -2
- package/types/runlist.d.ts +52 -2
- package/types/source.d.ts +13 -5
- package/types/text.d.ts +19 -0
- package/types/userrun.d.ts +10 -0
- package/types/worktree.d.ts +48 -0
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
import { checkHumanGuard } from './authorroutes.js';
|
|
15
15
|
import { readConfig, updateConfig, ConfigError } from '../config.js';
|
|
16
16
|
import { spendSummary, monthlyLimitOf } from '../monthly.js';
|
|
17
|
-
import {
|
|
17
|
+
import { spawn as realSpawn } from 'node:child_process';
|
|
18
|
+
import { loadKeysEnv, keysFilePath, keysHome, filledKeyNames, ensureKeysFile } from '../keysfile.js';
|
|
18
19
|
import { SHAPES, PRESET_KEY_NAMES, keyRows, endpointOf, defaultUrlOf, usableKey } from '../providerrows.js';
|
|
19
20
|
import { apiKeyProblem, checkProviderReachable } from '../providers.js';
|
|
20
21
|
|
|
@@ -22,8 +23,11 @@ import { apiKeyProblem, checkProviderReachable } from '../providers.js';
|
|
|
22
23
|
const DEEPSEEK_BALANCE_URL = 'https://api.deepseek.com/user/balance';
|
|
23
24
|
|
|
24
25
|
/**
|
|
26
|
+
* `openFolderSpawn` / `platform` are test seams for the Open keys folder button (a real caller passes
|
|
27
|
+
* neither: `child_process.spawn` and `process.platform`).
|
|
25
28
|
* @param {{ port: number, token: string, home?: string, env?: Record<string,string|undefined>,
|
|
26
|
-
* fetchImpl?: typeof fetch, now?: () => number
|
|
29
|
+
* fetchImpl?: typeof fetch, now?: () => number, openFolderSpawn?: (cmd: string, args: string[], o: object) => any,
|
|
30
|
+
* platform?: string }} opts
|
|
27
31
|
*/
|
|
28
32
|
export function createSettingsRoutes(opts) {
|
|
29
33
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
@@ -134,6 +138,8 @@ export function createSettingsRoutes(opts) {
|
|
|
134
138
|
placeholder: defaultUrlOf(r.provider),
|
|
135
139
|
keyStatus: !raw ? 'not set' : (raw === 'null' ? 'no key needed' : (problem ? `bad shape (${problem})` : 'found')),
|
|
136
140
|
canTest: r.provider !== 'gemini-api',
|
|
141
|
+
priceInPerM: typeof r.priceInPerM === 'number' ? r.priceInPerM : null,
|
|
142
|
+
priceOutPerM: typeof r.priceOutPerM === 'number' ? r.priceOutPerM : null,
|
|
137
143
|
tokens: s.tokensByRow[r.envName] ?? 0,
|
|
138
144
|
balance: r.provider === 'anthropic-api'
|
|
139
145
|
? { kind: 'note', usd: typeof note === 'number' && Number.isFinite(note) ? note : null }
|
|
@@ -148,11 +154,32 @@ export function createSettingsRoutes(opts) {
|
|
|
148
154
|
const row = currentRows(readConfig({ home: opts.home }).config).find((r) => r.envName === body?.envName);
|
|
149
155
|
if (!row) { send(400, { ok: false, error: 'unknown key — add NAME=key to your keys file and reload keys' }); return true; }
|
|
150
156
|
// only the three settings a row has; each is optional in the body, the whole triple is stored
|
|
157
|
+
/** @type {Record<string, string|number|null>} */
|
|
151
158
|
const patch = {
|
|
152
159
|
name: typeof body?.name === 'string' ? body.name.trim() : row.name,
|
|
153
160
|
shape: typeof body?.shape === 'string' ? body.shape : row.provider,
|
|
154
161
|
baseUrl: typeof body?.baseUrl === 'string' ? body.baseUrl.trim().replace(/\/+$/, '') : row.baseUrl,
|
|
155
162
|
};
|
|
163
|
+
// the two prices (USD per 1M tokens): absent = unchanged; blank / null = not set (the field is removed,
|
|
164
|
+
// never written as 0); otherwise a finite number >= 0 — anything else is refused and nothing is saved
|
|
165
|
+
for (const f of /** @type {const} */ (['priceInPerM', 'priceOutPerM'])) {
|
|
166
|
+
if (!Object.hasOwn(body ?? {}, f)) continue;
|
|
167
|
+
const v = body[f];
|
|
168
|
+
const label = f === 'priceInPerM' ? 'In' : 'Out';
|
|
169
|
+
if (v === null || (typeof v === 'string' && v.trim() === '')) { patch[f] = null; continue; }
|
|
170
|
+
const n = typeof v === 'number' ? v : (typeof v === 'string' && /^\d+(\.\d+)?$/.test(v.trim()) ? Number(v.trim()) : NaN);
|
|
171
|
+
if (!(Number.isFinite(n) && n >= 0)) {
|
|
172
|
+
send(400, { ok: false, error: `the ${label} price must be a number, 0 or more (USD per 1M tokens), or blank for not set` });
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
patch[f] = n;
|
|
176
|
+
}
|
|
177
|
+
// a price is both fields or neither — ratesFor refuses a half-set one at run time, so never save half
|
|
178
|
+
const after = (/** @type {'priceInPerM'|'priceOutPerM'} */ f) => (Object.hasOwn(patch, f) ? patch[f] !== null : row[f] !== undefined);
|
|
179
|
+
if (after('priceInPerM') !== after('priceOutPerM')) {
|
|
180
|
+
send(400, { ok: false, error: 'set both prices (In and Out, USD per 1M tokens) or neither — a half-set price is refused' });
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
156
183
|
try {
|
|
157
184
|
updateConfig({ keys: { [row.envName]: patch } }, { home: opts.home });
|
|
158
185
|
} catch (e) {
|
|
@@ -164,6 +191,29 @@ export function createSettingsRoutes(opts) {
|
|
|
164
191
|
return true;
|
|
165
192
|
}
|
|
166
193
|
|
|
194
|
+
if (pathname === '/api/settings/open-keys-folder' && req.method === 'POST') {
|
|
195
|
+
// opens the FOLDER (never the file, never an editor) in the file manager; argv array, no shell, detached.
|
|
196
|
+
// Another OS, or a failed spawn: say so and hand back the path, which the page then shows as text.
|
|
197
|
+
const dir = keysHome(opts.home);
|
|
198
|
+
const platform = opts.platform ?? process.platform;
|
|
199
|
+
const cmd = platform === 'linux' ? 'xdg-open' : (platform === 'darwin' ? 'open' : null);
|
|
200
|
+
const cannot = () => send(200, { ok: true, opened: false, path: dir });
|
|
201
|
+
if (!cmd) { cannot(); return true; }
|
|
202
|
+
let child;
|
|
203
|
+
try {
|
|
204
|
+
child = (opts.openFolderSpawn ?? realSpawn)(cmd, [dir], { detached: true, stdio: 'ignore' });
|
|
205
|
+
} catch { cannot(); return true; }
|
|
206
|
+
let settled = false;
|
|
207
|
+
child.once('error', () => { if (!settled) { settled = true; cannot(); } });
|
|
208
|
+
child.once('spawn', () => {
|
|
209
|
+
if (settled) return;
|
|
210
|
+
settled = true;
|
|
211
|
+
if (typeof child.unref === 'function') child.unref();
|
|
212
|
+
send(200, { ok: true, opened: true, path: dir });
|
|
213
|
+
});
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
167
217
|
if (pathname === '/api/settings/providers/balance-note' && req.method === 'POST') {
|
|
168
218
|
// the Anthropic balance is a NOTE the person types; no check ever reads it
|
|
169
219
|
const raw = body?.usd;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// PANEL status words — ONE code-owned table (sign -> word) and ONE writer of "which word does this run wear".
|
|
2
|
+
// hamr's ruling 2026-10-04 (option A): every surface (run cards, expanded run rows, the right-side header, imported
|
|
3
|
+
// rows) reads `status` from the server; the page never maps an outcome to a word itself.
|
|
4
|
+
//
|
|
5
|
+
// [▶] running · [·] waiting · [✓] passed · [✗] failed · [✗] capped · [✗] stopped · [?] died
|
|
6
|
+
//
|
|
7
|
+
// Outcome -> word (every outcome the panel knows; anything not listed is `failed`):
|
|
8
|
+
// green, already-green, satisfied -> passed
|
|
9
|
+
// no job-end, runner alive (or starting) -> running
|
|
10
|
+
// no job-end, runner gone (died) -> died
|
|
11
|
+
// cap-halt, wall-halt -> capped (money cap / time cap)
|
|
12
|
+
// escalated + money-halt record, escalated + wall-halt -> capped
|
|
13
|
+
// stopped (the person pressed Stop) -> stopped
|
|
14
|
+
// plan-red, check-red, step-red, escalated (strikes / provider / other), close-red, provider-red, step-stalled,
|
|
15
|
+
// and every other outcome (refusals, instrument stops) -> failed
|
|
16
|
+
// `waiting` is the step-level sign for a step that has not started; no run outcome maps to it.
|
|
17
|
+
|
|
18
|
+
/** the sentence a passed run carries as its reason (the Ended block's `line`) */
|
|
19
|
+
export const GOAL_MET_LINE = 'goal met';
|
|
20
|
+
|
|
21
|
+
/** @typedef {'running'|'waiting'|'passed'|'failed'|'capped'|'stopped'|'died'} StatusKey */
|
|
22
|
+
|
|
23
|
+
/** the table: key -> sign + word */
|
|
24
|
+
export const STATUS = Object.freeze({
|
|
25
|
+
running: Object.freeze({ sign: '▶', word: 'running' }),
|
|
26
|
+
waiting: Object.freeze({ sign: '·', word: 'waiting' }),
|
|
27
|
+
passed: Object.freeze({ sign: '✓', word: 'passed' }),
|
|
28
|
+
failed: Object.freeze({ sign: '✗', word: 'failed' }),
|
|
29
|
+
capped: Object.freeze({ sign: '✗', word: 'capped' }),
|
|
30
|
+
stopped: Object.freeze({ sign: '✗', word: 'stopped' }),
|
|
31
|
+
died: Object.freeze({ sign: '?', word: 'died' }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The status of one run.
|
|
36
|
+
* @param {{outcome: string|null|undefined, died?: boolean, category?: string|null, moneyHalt?: boolean}} o
|
|
37
|
+
* `category` is the last escalation's category; `moneyHalt` is whether the spine carries a `money-halt` record
|
|
38
|
+
* @returns {{key: StatusKey, sign: string, word: string}}
|
|
39
|
+
*/
|
|
40
|
+
export function statusFor(o) {
|
|
41
|
+
/** @type {StatusKey} */
|
|
42
|
+
let key = 'failed';
|
|
43
|
+
const out = o.outcome;
|
|
44
|
+
if (o.died) key = 'died';
|
|
45
|
+
else if (out === null || out === undefined) key = 'running';
|
|
46
|
+
else if (out === 'green' || out === 'already-green' || out === 'satisfied') key = 'passed';
|
|
47
|
+
else if (out === 'cap-halt' || out === 'wall-halt') key = 'capped';
|
|
48
|
+
else if (out === 'stopped') key = 'stopped';
|
|
49
|
+
else if (out === 'escalated' && ((o.category === 'cap-halt' && o.moneyHalt) || o.category === 'wall-halt')) key = 'capped';
|
|
50
|
+
return { key, ...STATUS[key] };
|
|
51
|
+
}
|
package/src/planrun.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// which putting either in fs.deny would silently contradict.
|
|
22
22
|
|
|
23
23
|
import { createRequire } from 'node:module';
|
|
24
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
24
|
+
import { readFileSync, readdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
25
25
|
import { createHash } from 'node:crypto';
|
|
26
26
|
import { join, resolve, relative } from 'node:path';
|
|
27
27
|
import { Gate } from 'bareguard';
|
|
@@ -39,8 +39,9 @@ import { TOOL_BY_VERB, CTX_TOOLS, createCtxTools, toolAction, PERSONA_TOOLS, str
|
|
|
39
39
|
import { createReadShim, readShimArm, readShimStrategy } from './readshim.js';
|
|
40
40
|
import { globToPrefix, redactSecrets, SECRET_PATTERNS } from './validate.js';
|
|
41
41
|
import { validateBridge, loadGate, newestEligibleVersion, reuseEligibility, quarantinesCredit, QUARANTINED_CODE } from './bridges.js';
|
|
42
|
-
import { extractArtifact } from './text.js';
|
|
42
|
+
import { extractArtifact, rateSourceFields } from './text.js';
|
|
43
43
|
import { defaultJudgeLoop } from './judged.js';
|
|
44
|
+
export { rateSourceFields };
|
|
44
45
|
import { createClock, isWallTimeout } from './clock.js';
|
|
45
46
|
import { isDeclaredClose, runDeclaredStages, validateCloseDecl, closeGrade, HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED } from './declaredclose.js';
|
|
46
47
|
import {
|
|
@@ -536,25 +537,8 @@ export const FORBIDDEN_WRITE_SEGMENT_PATTERN = /"path":"(?:(?:[^"\\]|\\.)*\/)?(?
|
|
|
536
537
|
*/
|
|
537
538
|
export const BOUND_REASON_MAX = 200;
|
|
538
539
|
|
|
539
|
-
/**
|
|
540
|
-
|
|
541
|
-
* `spendProvenance` in src/ledger.js). bare-agent >=0.37 rides `rateSource` beside
|
|
542
|
-
* `pricing` on every metering payload; both metering callbacks below forward it onto the
|
|
543
|
-
* spine through THIS one helper, because two sites spelling one rule are two instruments
|
|
544
|
-
* (the ripgrep fix that landed in ci.yml but not publish.yml).
|
|
545
|
-
*
|
|
546
|
-
* Forwarded VERBATIM — the write side reports what upstream said, and src/ledger.js is
|
|
547
|
-
* the one place that decides what counts as a guess. And an ABSENT provenance stays
|
|
548
|
-
* absent: a payload carrying no `rateSource` (bare-agent <0.37, and every round already
|
|
549
|
-
* in the archive) emits no field at all, so a reader sees UNKNOWN rather than a label we
|
|
550
|
-
* invented. Never defaulted to `null` here — `null` is upstream saying "nothing was
|
|
551
|
-
* priced", which is a different fact from "nobody told us".
|
|
552
|
-
* @param {any} arg an `onLlmResult` / `onTurn` payload
|
|
553
|
-
* @returns {{rateSource?: string|null}} the field to spread into the spine record
|
|
554
|
-
*/
|
|
555
|
-
export function rateSourceFields(arg) {
|
|
556
|
-
return arg && typeof arg === 'object' && 'rateSource' in arg ? { rateSource: arg.rateSource } : {};
|
|
557
|
-
}
|
|
540
|
+
/** the recorded reason for a step attempt whose round was cut at the output cap (F122 2A b) */
|
|
541
|
+
export const TRUNCATED_REASON = 'output cut at the token limit (max_tokens)';
|
|
558
542
|
|
|
559
543
|
/**
|
|
560
544
|
* What the NEXT attempt is told about the bound that cut the previous one.
|
|
@@ -589,6 +573,9 @@ export function rateSourceFields(arg) {
|
|
|
589
573
|
*/
|
|
590
574
|
export function boundedNote(bounded, rounds) {
|
|
591
575
|
if (!bounded) return null;
|
|
576
|
+
if (bounded.cause === 'truncated') {
|
|
577
|
+
return `Your previous attempt was CUT OFF: ${TRUNCATED_REASON}. That response was discarded.`;
|
|
578
|
+
}
|
|
592
579
|
if (bounded.cause !== 'denied') {
|
|
593
580
|
return `Your previous attempt was CUT OFF after ${rounds} tool rounds. `
|
|
594
581
|
+ 'Reading is bounded; writing is not. Form a hypothesis EARLY and make the change.';
|
|
@@ -982,12 +969,18 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
982
969
|
* passes `false` so the three `--resume`-naming readouts below (`MONEY_OPTIONS`,
|
|
983
970
|
* the resume-plan-red option, the fix-loop terminal) say "resume is `run-u`-only
|
|
984
971
|
* in v1" instead of naming a flag that would fail if typed.
|
|
972
|
+
* @param {string|null} [opts.stopFile=null] P5 item 5 — the run's STOP REQUEST file
|
|
973
|
+
* (`stopFilePath(spine)`, src/legs.js). Read at ONE seam, the round boundary of a step worker
|
|
974
|
+
* (the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
|
|
975
|
+
* the file, end the Loop after that round, and end the leg `stopped` (a checkpoint outcome, filed
|
|
976
|
+
* the way a mid-step cap-halt is; resume re-enters that step). Nothing is in flight at that
|
|
977
|
+
* point, so the spend is exact. `null` = no stop surface (the default).
|
|
985
978
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
986
979
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
987
|
-
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
980
|
+
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'stopped' | 'provider-red' | 'interpreter-red' |
|
|
988
981
|
* 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
989
982
|
*/
|
|
990
|
-
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, rates = null, judgeRates = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, closeDir = null, maxStepRounds = 40, layerRoot = false, readShim = false, scout = true, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, priorSpentUsd = 0, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
983
|
+
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, rates = null, judgeRates = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, closeDir = null, maxStepRounds = 40, layerRoot = false, readShim = false, scout = true, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, priorSpentUsd = 0, reviewDoor = null, doorRerun = null, resumable = true, stopFile = null }) {
|
|
991
984
|
// MEMORY-CACHE: what the read shim (src/readshim.js) saved THIS run, summed across
|
|
992
985
|
// every mkWorker's own shim instance (scout, drafter, each step's worker, the fix
|
|
993
986
|
// worker) — one accumulator closed over by all of them, because the shim's ledger
|
|
@@ -2434,7 +2427,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2434
2427
|
* `iteration` mirrors `roundIteration` exactly, undefined included: the bound
|
|
2435
2428
|
* cannot know a label the run never set, and inventing one here would be the
|
|
2436
2429
|
* only place in this file that claims to.
|
|
2437
|
-
* @type {{iteration: number|string|undefined, cause: 'rounds'|'wall'|'denied', reason: string|null}|undefined}
|
|
2430
|
+
* @type {{iteration: number|string|undefined, cause: 'rounds'|'wall'|'denied'|'truncated', reason: string|null}|undefined}
|
|
2438
2431
|
*/
|
|
2439
2432
|
let attemptBounded;
|
|
2440
2433
|
/**
|
|
@@ -2658,6 +2651,14 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2658
2651
|
// completes no round and returns from no step, which are the only two places
|
|
2659
2652
|
// `clock.expired()` is otherwise consulted). Same clock object as every other
|
|
2660
2653
|
// reader — never a second time source.
|
|
2654
|
+
/** P5 item 5 — set by the round seam when the person's stop request was honoured on this worker */
|
|
2655
|
+
let stopHit = false;
|
|
2656
|
+
const throwIfStopped = () => {
|
|
2657
|
+
if (!stopHit) return;
|
|
2658
|
+
const err = /** @type {CategorizedError} */ (new Error('stopped at the person\'s request (a stop request was honoured at a round boundary)'));
|
|
2659
|
+
err.category = 'stopped';
|
|
2660
|
+
throw err;
|
|
2661
|
+
};
|
|
2661
2662
|
const stallWatch = createStallWatch({
|
|
2662
2663
|
onStall: (n) => emit('stall', { phase, iteration: roundIteration, stall: n, stallMs: STALL_MS, maxStalls: MAX_STALLS }),
|
|
2663
2664
|
expired: () => clock.expired(),
|
|
@@ -2740,7 +2741,17 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2740
2741
|
// step loop after a step returns (between steps), the head of a step's attempt
|
|
2741
2742
|
// (a step that would BEGIN past the deadline is never funded), and the head of
|
|
2742
2743
|
// a close-fix iteration (W-2 — the run stops on the verdict already minted).
|
|
2743
|
-
|
|
2744
|
+
// P5 item 5 — the person's STOP, read at THIS seam: the round boundary, where the money cap binds
|
|
2745
|
+
// (nothing in flight, the round just metered). It is the ONE owner of the stop request. The file is
|
|
2746
|
+
// consumed here; `self.stop()` ends the Loop after this round exactly as a bound does; `ask`/`askFrom`
|
|
2747
|
+
// then throw category `stopped`, which the step loop files as the checkpoint outcome `stopped` the way a
|
|
2748
|
+
// mid-step cap-halt is filed. Step workers only (scout/plan/fix never read it).
|
|
2749
|
+
if (stopFile !== null && !stopHit && phase.startsWith('step:') && existsSync(stopFile)) {
|
|
2750
|
+
try { unlinkSync(stopFile); } catch { /* already consumed */ }
|
|
2751
|
+
stopHit = true;
|
|
2752
|
+
emit('stop-requested', { phase, step: phase.slice('step:'.length), iteration: roundIteration, round: roundsThisAttempt, meaning: 'the person asked to stop; the leg ends after this round and a resume re-enters this step' });
|
|
2753
|
+
self.stop();
|
|
2754
|
+
} else if (clock.expired()) {
|
|
2744
2755
|
attemptBounded = { iteration: roundIteration, cause: 'wall', reason: null };
|
|
2745
2756
|
emit('wall-bounded', { phase, iteration: roundIteration, ...clock.report(closeTimeoutForReport) });
|
|
2746
2757
|
self.stop();
|
|
@@ -2777,7 +2788,9 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2777
2788
|
// bare-agent's own Loop.chat() (loop.js ~1321) strips a leading system
|
|
2778
2789
|
// message before re-running for exactly this reason; do the same here.
|
|
2779
2790
|
const rest = msgs[0]?.role === 'system' ? msgs.slice(1) : msgs;
|
|
2780
|
-
|
|
2791
|
+
const r = await stallWatch.watch((gen) => newLoop(gen).run([...rest, { role: 'user', content: prompt }], [], { cacheMessages: true, maxTokens: 32000, ...callBounds() }));
|
|
2792
|
+
throwIfStopped();
|
|
2793
|
+
return r;
|
|
2781
2794
|
} catch (e) {
|
|
2782
2795
|
throw categorize(e).err;
|
|
2783
2796
|
}
|
|
@@ -2786,6 +2799,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2786
2799
|
let r;
|
|
2787
2800
|
try {
|
|
2788
2801
|
r = await stallWatch.watch((gen) => newLoop(gen).run([{ role: 'user', content: prompt }], defs, { cacheMessages: true, maxTokens: 32000, ...callBounds() }));
|
|
2802
|
+
throwIfStopped();
|
|
2789
2803
|
} catch (e) {
|
|
2790
2804
|
throw categorize(e).err;
|
|
2791
2805
|
}
|
|
@@ -2803,6 +2817,17 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2803
2817
|
emit('attempt-bounded', { phase, iteration: roundIteration, rounds: roundsThisAttempt, cap: attemptRounds, reason });
|
|
2804
2818
|
return r;
|
|
2805
2819
|
}
|
|
2820
|
+
// F122 "2A" half (b), hamr 2026-10-05: a STEP or FIX worker round cut at max_tokens
|
|
2821
|
+
// is a FAILED WORKER ATTEMPT, not transport. Same lane as the deny streak above: the
|
|
2822
|
+
// cut round's output is discarded (bare-agent already dropped it), its spend is booked
|
|
2823
|
+
// by the per-round meter, the tree is judged as it stands, and the step's strike
|
|
2824
|
+
// ladder (or the fix governor) applies unchanged (hamr ruling A extended it to `fix`
|
|
2825
|
+
// after run muux1x96). Scout, drafter, replan keep the transport route below.
|
|
2826
|
+
if (r.error && r.error.startsWith('truncated:') && (phase.startsWith('step:') || phase === 'fix')) {
|
|
2827
|
+
attemptBounded = { iteration: roundIteration, cause: 'truncated', reason: TRUNCATED_REASON };
|
|
2828
|
+
emit('attempt-bounded', { phase, iteration: roundIteration, rounds: roundsThisAttempt, cap: attemptRounds, reason: TRUNCATED_REASON, stopReason: r.stopReason ?? null });
|
|
2829
|
+
return r;
|
|
2830
|
+
}
|
|
2806
2831
|
// the remaining error-return taxonomy (one map, same doctrine as native's):
|
|
2807
2832
|
// halt → cap-halt, API truncation → provider-red, anything else is wiring
|
|
2808
2833
|
if (r.error) {
|
|
@@ -3690,6 +3715,11 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3690
3715
|
// (attempts used, steps done); its absence means the deadline landed inside a call
|
|
3691
3716
|
// instead, which is F64's cutMidCall stop — and run.js keys the job-end money floor
|
|
3692
3717
|
// on exactly that field, so conflating the two would report an unknown as exact.
|
|
3718
|
+
// P5 item 5 — the person stopped the leg at a round boundary inside this step: a checkpoint, not a verdict
|
|
3719
|
+
if (cat === 'stopped') {
|
|
3720
|
+
planExecuted();
|
|
3721
|
+
return 'stopped';
|
|
3722
|
+
}
|
|
3693
3723
|
if (cat === 'wall-halt') {
|
|
3694
3724
|
emitWallHalt(stepWallStop ?? { cutMidCall: true, phase: `step:${step.id}`, stepsDone: idx, stepsPlanned: plan.steps.length });
|
|
3695
3725
|
planExecuted();
|
|
@@ -4053,6 +4083,9 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
4053
4083
|
.filter((s) => s.values.length > 0)
|
|
4054
4084
|
.map((s) => `${s.stage} ${s.values.join(' → ')}`);
|
|
4055
4085
|
const factsLine = [checksFact, ...stageFacts].filter(Boolean).join(' · ');
|
|
4086
|
+
// same iteration numbering as the step loop: `w.setIteration(iteration)` above
|
|
4087
|
+
// stamps the bound, so "the attempt just before this one" is `iteration - 1`
|
|
4088
|
+
const bound = w.wasBounded();
|
|
4056
4089
|
await w.ask([
|
|
4057
4090
|
'The job\'s final verification is failing. Fix the repository so it passes.',
|
|
4058
4091
|
`Repository root (absolute): ${workdir}\nEvery path you pass to a tool MUST be absolute and inside this root.`,
|
|
@@ -4060,6 +4093,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
4060
4093
|
!gap && post.gap && `The verification's output on the tree as it stands (not an attempt of yours):\n${post.gap}`,
|
|
4061
4094
|
gap && `Previous attempt failed the verification:\n${gap}`,
|
|
4062
4095
|
factsLine && `The close's own numbers so far, oldest first (facts only — no advice):\n${factsLine}`,
|
|
4096
|
+
bound?.iteration === iteration - 1 && boundedNote(bound, maxStepRounds),
|
|
4063
4097
|
rootInj && rootInj.note,
|
|
4064
4098
|
].filter(Boolean).join('\n\n'));
|
|
4065
4099
|
};
|
package/src/providerrows.js
CHANGED
|
@@ -134,6 +134,23 @@ export function endpointOf(provider, baseUrl) {
|
|
|
134
134
|
} catch { return raw.replace(/\/+$/, ''); }
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* A job's provider in plain words for a refusal line: the host its Base URL points at (else the
|
|
139
|
+
* shape's own default host, else the shape's label), plus the model when the spec names one.
|
|
140
|
+
* Never a key, a path or a status code.
|
|
141
|
+
* @param {{provider?: string, baseUrl?: string|null, model?: string|null}} spec
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
export function providerInWords(spec) {
|
|
145
|
+
const provider = String(spec?.provider ?? '');
|
|
146
|
+
const url = endpointOf(provider, spec?.baseUrl ?? null);
|
|
147
|
+
let host = '';
|
|
148
|
+
try { host = new URL(url).host; } catch { host = ''; }
|
|
149
|
+
if (host === '') host = SHAPES.find((s) => s.id === provider)?.label ?? provider;
|
|
150
|
+
const model = typeof spec?.model === 'string' ? spec.model.trim() : '';
|
|
151
|
+
return model !== '' ? `${host}, ${model}` : host;
|
|
152
|
+
}
|
|
153
|
+
|
|
137
154
|
/**
|
|
138
155
|
* The URL a row talks to, for display ('' = the vendor's own host, which has no URL to show).
|
|
139
156
|
* @param {KeyRow} row
|
package/src/ralph.js
CHANGED
|
@@ -49,7 +49,7 @@ const CLOSE_KILL_GRACE_MS = 2000;
|
|
|
49
49
|
* @param {{env: NodeJS.ProcessEnv, cwd?: string, timeoutMs?: number}} o
|
|
50
50
|
* @returns {Promise<{error: (Error & {code?: string})|null, status: number|null, signal: string|null, stdout: string, stderr: string}>}
|
|
51
51
|
*/
|
|
52
|
-
function spawnClose(cmd, args, { env, cwd, timeoutMs }) {
|
|
52
|
+
export function spawnClose(cmd, args, { env, cwd, timeoutMs }) {
|
|
53
53
|
return new Promise((resolve) => {
|
|
54
54
|
const child = spawn(cmd, args, { env, cwd });
|
|
55
55
|
/** @type {Buffer[]} */ const outChunks = [];
|
|
@@ -653,6 +653,9 @@ export async function ralph({ middle, close, judge, capRuns, ladder, emit, redac
|
|
|
653
653
|
['raise maxWallMs and rerun (resume-to-cap; a spec edit, so the new hash needs re-approval)',
|
|
654
654
|
'revise the goal/spec so the work fits the time (same re-approval)',
|
|
655
655
|
'abandon the task']],
|
|
656
|
+
// P5 item 5 — the PERSON asked to stop at a round boundary: nothing failed, nothing is discarded.
|
|
657
|
+
'stopped': ['You stopped the run. Nothing is discarded: the work on disk stands, and a resume re-enters this step.',
|
|
658
|
+
['resume (the same run continues)', 'abandon the task']],
|
|
656
659
|
// F66 — the STALL WATCHDOG gave up: no round completed for the stall window,
|
|
657
660
|
// three times over, and reissuing the call did not recover it. It borrows both
|
|
658
661
|
// siblings' rules. Like `step-variance`, the caller reads this category and
|