bareloop 0.13.0 → 0.14.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 +46 -0
- package/README.md +1 -1
- package/bareloop.context.md +63 -5
- package/package.json +2 -2
- package/src/behaviour.js +150 -0
- package/src/bridges.js +1 -1
- package/src/calibrate.js +1 -1
- package/src/cardauthor.js +1 -1
- package/src/index.js +5 -0
- package/src/job.js +1 -1
- package/src/judged.js +1 -1
- package/src/ledger.js +1 -1
- package/src/planrun.js +140 -3
- package/src/readshim.js +39 -7
- package/src/reuse.js +24 -2
- package/src/root.js +1 -1
- package/src/run.js +15 -2
- package/src/selection.js +1 -1
- package/src/transport.js +71 -0
- package/types/behaviour.d.ts +46 -0
- package/types/index.d.ts +1 -0
- package/types/readshim.d.ts +17 -6
- package/types/reuse.d.ts +22 -0
- package/types/transport.d.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ All notable changes to bareloop are documented here. Format:
|
|
|
5
5
|
[SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
|
|
6
6
|
feature lands, **patch** = docs, fixes, scaffolding.
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.14.0] — 2026-08-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **One transport-class retry** (F115, hamr's ruling): the ONE worker-Loop seam in
|
|
15
|
+
`src/planrun.js` now retries exactly once on a transport-only throw (TLS fault,
|
|
16
|
+
`ECONNRESET`/`EPIPE`/`ETIMEDOUT`, `fetch failed` over a network cause — never an HTTP
|
|
17
|
+
4xx/5xx/429 response), classified by the new `src/transport.js` (`isTransportFailure`,
|
|
18
|
+
fixed `TRANSPORT_MAX_ATTEMPTS`, never operator-configurable). Emits a report-only
|
|
19
|
+
`transport-retry` spine record and forces `job-end.spendComplete: false` for the rest of the
|
|
20
|
+
run — the retried attempt's first throw may already have been billed — surfaced by
|
|
21
|
+
`scripts/run-u.mjs` as a `retries` line only when a retry occurred.
|
|
22
|
+
- **provider-red joins the resumable set** (F115, PRD v1.80 TODO #4): when the one transport
|
|
23
|
+
retry also fails and a `run-u` run ends `provider-red`, its tail now prints an honest
|
|
24
|
+
readout — spend so far as a floor, "died in step X of Y" (or "before a plan was accepted"),
|
|
25
|
+
and the exact `--resume` command — and `--resume` re-enters at the recorded step with the
|
|
26
|
+
accepted plan, folding prior spend in. No cost/step threshold gates the offer (the operator
|
|
27
|
+
decides, every time); verdict terminals (`escalated`/`step-red`/`plan-red`) still refuse.
|
|
28
|
+
Folded from a local `scripts/run-u.mjs` widening into the library's own canonical
|
|
29
|
+
`CHECKPOINT_OUTCOMES` (`src/reuse.js`) so the two lists cannot drift.
|
|
30
|
+
- **Run behaviour summary** (build-list item 2, agreed 2026-08-23): `runBehaviour(events, {
|
|
31
|
+
runId? })` and `formatBehaviour(summary)` in `src/behaviour.js`, plus
|
|
32
|
+
`scripts/behaviour-readout.mjs <gate-audit.jsonl> [run_id]`. A report-only text block —
|
|
33
|
+
"94 tool calls · 61 read, 28 grep, 5 recall" / "17 exact repeats (~18%)" — computed entirely
|
|
34
|
+
from a run's own gate-audit JSONL. Gates nothing, changes no verdict, writes nothing to the
|
|
35
|
+
spine. The exact-repeat key is the full recorded action (`type` + `path` + every `args` key),
|
|
36
|
+
never collapsed to `{type, path}` — sharper collapse would have merged different byte ranges
|
|
37
|
+
of the same file into false repeats.
|
|
38
|
+
- **`memory-cache` end-of-run readout** for the read shim: `runPlan` emits one spine record
|
|
39
|
+
(`{pointered, capped, bytesWithheld, approxTokens}`) summing what L1's pointer and L4's cap
|
|
40
|
+
withheld across the whole run, only when the shim is armed. `scripts/run-u.mjs` prints it as
|
|
41
|
+
`MEMORY-CACHE N re-reads answered from memory · M reads capped · X KB withheld (~Tk tokens
|
|
42
|
+
not re-sent)`; `scripts/behaviour-readout.mjs` prints the same line given the run's spine as
|
|
43
|
+
an optional third positional. No cost fields — report-only, not a spend record.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **`scripts/readshim-battery.mjs`'s spend reader compared a resumed leg's own money against
|
|
48
|
+
the wrong job-end figure** (F116): `readSpend`'s `ledgerUsd` now reads
|
|
49
|
+
`job-end.engagementSpentUsd` (this leg's spend) when a spine carries one, falling back to
|
|
50
|
+
`job-end.spentUsd` only for a cold run — `spentUsd` on a resumed job-end is a CHAIN total,
|
|
51
|
+
not this spine's own spend. `chainUsd`/`priorUsd` are exposed as new, honest fields and never
|
|
52
|
+
folded into `accountedUsd`/`ledgerUsd`/`spendUsd`.
|
|
53
|
+
|
|
8
54
|
## [0.13.0] — 2026-08-23
|
|
9
55
|
|
|
10
56
|
### Fixed (2026-08-23 — the release round: a pointer that lied, a close with no home, a rule that charged for its own refusal)
|
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ package, and gaps get fixed upstream rather than shimmed.
|
|
|
162
162
|
bareloop is the productization of **[adaptlearn](https://github.com/hamr0/adaptlearn)**, a
|
|
163
163
|
closed experimental record on whether agent-authored workflows improve under an honest gate.
|
|
164
164
|
The design laws it inherited, and the open questions it has not answered yet, are in
|
|
165
|
-
[`docs/
|
|
165
|
+
[`docs/product/PRD.md`](docs/product/PRD.md).
|
|
166
166
|
|
|
167
167
|
## License
|
|
168
168
|
|
package/bareloop.context.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
> where `hitl` IS admitted, `human-confirms` IS live, and the three `hitl-*` terminals exist —
|
|
27
27
|
> integrate against what is written here, and expect the class-level rename when the next rung
|
|
28
28
|
> lands. Records: PRD Addendum v1.71 and
|
|
29
|
-
> `docs/
|
|
29
|
+
> `docs/product/2026-08-17-softgreen-review-door-design.md` (repo-only, not shipped);
|
|
30
30
|
> evidence F102–F105.
|
|
31
31
|
|
|
32
32
|
## What this is
|
|
@@ -1093,13 +1093,25 @@ raises it.
|
|
|
1093
1093
|
of PRICED rounds ONLY — never an estimate from token counts or averages (cap-not-estimate) —
|
|
1094
1094
|
and it is stated even on the pre-token reds, where the honest figure is a real `0`.
|
|
1095
1095
|
`spendComplete` says whether that figure is EXACT; `false` means `spentUsd` is a FLOOR ("at
|
|
1096
|
-
least $X", true total unknowable) and must not be read as a total.
|
|
1096
|
+
least $X", true total unknowable) and must not be read as a total. Five things set it, all
|
|
1097
1097
|
the same class — a call whose cost is unknowable from here: a round that came back unpriced
|
|
1098
1098
|
(F6); a run that ABSORBED a stall, because the fuse's silent reissue may pay twice for one
|
|
1099
1099
|
answer and the flag is one-way whatever the outcome; a `wall-halt` that cut a call
|
|
1100
|
-
mid-flight (`cutMidCall`);
|
|
1100
|
+
mid-flight (`cutMidCall`); a RESUME whose fold was itself a floor
|
|
1101
1101
|
(`priorSpendComplete: false` — an unknown does not heal by being carried forward, so every
|
|
1102
|
-
round of this attempt being priced repairs nothing about the one before it)
|
|
1102
|
+
round of this attempt being priced repairs nothing about the one before it); and a
|
|
1103
|
+
**transport retry** (F115, hamr's ruling: "one retry for transport layer failure and
|
|
1104
|
+
reporting with the rest end of gate"). Every worker Loop (scout, drafter, each step, the fix
|
|
1105
|
+
worker — the ONE seam in `planrun.js`) gets exactly ONE extra attempt, and only for a
|
|
1106
|
+
transport-class throw — fetch itself throwing with no HTTP response at all (a TLS fault,
|
|
1107
|
+
`ECONNRESET`/`EPIPE`/`ETIMEDOUT`, `fetch failed` wrapping a network cause); an HTTP response
|
|
1108
|
+
(4xx/5xx/429) is never retried here and rides bare-agent's own unchanged policy. The budget is
|
|
1109
|
+
fixed (`TRANSPORT_MAX_ATTEMPTS` in `src/transport.js`) — never a job-spec or CLI knob, tighten-only
|
|
1110
|
+
doctrine. Each retry emits a report-only `transport-retry` spine record (`{phase, attempt, error,
|
|
1111
|
+
recovered}`, no cost field — the ledger is unaffected); the FIRST attempt threw before any usage
|
|
1112
|
+
figure came back, so **any** transport retry floors `spendComplete` for the rest of the run even
|
|
1113
|
+
when the retry recovers and the run finishes green — the same one-way flag class as `stalled`
|
|
1114
|
+
and `cutMidCall`. A wall stop read BETWEEN iterations or steps has no call in flight,
|
|
1103
1115
|
so it stays exact. Both fields are present on all outcomes, so a consumer never branches on
|
|
1104
1116
|
field presence and never has to launder a missing `spentUsd` into `$0`.
|
|
1105
1117
|
|
|
@@ -2084,6 +2096,52 @@ $0). `VOUCHED_RATE_SOURCES` is the frozen allow-list they key on. **Reporting on
|
|
|
2084
2096
|
no class, no halt. CLI lands at N5; the
|
|
2085
2097
|
panel reads the same file at N6.
|
|
2086
2098
|
|
|
2099
|
+
### `runBehaviour(events, { runId? })` / `formatBehaviour(summary)` — `src/behaviour.js`
|
|
2100
|
+
|
|
2101
|
+
Build-list item 2 (agreed 2026-08-23): one report-only text block summarizing what a run
|
|
2102
|
+
DID with its tools, computed entirely from the run's own gate-audit JSONL. **Gates nothing,
|
|
2103
|
+
changes no verdict, writes NOTHING to the spine** — a pure function over records that already
|
|
2104
|
+
exist, same posture as `spendProvenance` above. `runBehaviour` counts and returns the summary
|
|
2105
|
+
object; `formatBehaviour` renders it as the printed block:
|
|
2106
|
+
|
|
2107
|
+
```
|
|
2108
|
+
94 tool calls · 61 read, 28 grep, 5 recall
|
|
2109
|
+
17 exact repeats (~18%)
|
|
2110
|
+
```
|
|
2111
|
+
|
|
2112
|
+
`llm` action rows are provider rounds, not tool calls, and are always excluded; only
|
|
2113
|
+
`decision: 'allow'` rows count toward the total (a `deny` is tallied separately as `N denied`,
|
|
2114
|
+
shown only when nonzero). **The exact-repeat key is the FULL recorded action — `type` + `path`
|
|
2115
|
+
+ every key of `args`** (sorted, stably serialized), never collapsed to `{type, path}`: if the
|
|
2116
|
+
gate ever starts recording byte ranges or offsets, two different slices of one file must not
|
|
2117
|
+
read as the same repeated call. Repeat detection is therefore exactly as sharp as what the gate
|
|
2118
|
+
records, no sharper — a tool that reads identical bytes through two differently-shaped argument
|
|
2119
|
+
objects will not be caught by this. `scripts/behaviour-readout.mjs <gate-audit.jsonl> [run_id]`
|
|
2120
|
+
prints the block for one run or every run_id found in the file, skipping malformed lines with
|
|
2121
|
+
a count rather than throwing.
|
|
2122
|
+
|
|
2123
|
+
```js
|
|
2124
|
+
import { runBehaviour, formatBehaviour } from 'bareloop';
|
|
2125
|
+
const events = fs.readFileSync(auditFile, 'utf8').trim().split('\n').map(JSON.parse);
|
|
2126
|
+
console.log(formatBehaviour(runBehaviour(events, { runId })));
|
|
2127
|
+
```
|
|
2128
|
+
|
|
2129
|
+
#### `memory-cache` — the read shim's own end-of-run readout (`src/readshim.js`, `src/planrun.js`)
|
|
2130
|
+
|
|
2131
|
+
A single spine record, emitted once by `runPlan` right before it returns (so it lands on the
|
|
2132
|
+
spine before the caller's `job-end`), reporting what the READ SHIM (L1 pointer + L4 cap) saved
|
|
2133
|
+
this run: `{pointered, capped, bytesWithheld, approxTokens}`. `pointered` counts re-reads of
|
|
2134
|
+
unchanged content answered with a pointer instead of the bytes; `capped` counts reads served as
|
|
2135
|
+
a bounded slice; `bytesWithheld` sums exactly the bytes NOT re-sent in both cases (never
|
|
2136
|
+
estimated) — the diff lever (L2) isn't counted, it's a different response shape. `approxTokens`
|
|
2137
|
+
is `Math.round(bytesWithheld / 4)`, a bytes→tokens ESTIMATE named accordingly so nothing reads
|
|
2138
|
+
it as measured. **Emitted only when the shim is armed** (any `readShim` arm but the off one) —
|
|
2139
|
+
an unarmed run emits no record at all, never a fabricated zero. It carries no cost fields, so it
|
|
2140
|
+
is not a spend record and no spend-slicing instrument needs to account for it.
|
|
2141
|
+
`scripts/run-u.mjs` prints it as a `MEMORY-CACHE` line right after `BEHAVIOUR` when armed
|
|
2142
|
+
(`no record` if the run ended before the summary could fire); `scripts/behaviour-readout.mjs`
|
|
2143
|
+
prints the same line when handed the run's spine file as its optional third positional.
|
|
2144
|
+
|
|
2087
2145
|
## Architecture
|
|
2088
2146
|
|
|
2089
2147
|
Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
|
|
@@ -2115,7 +2173,7 @@ is disclosed to the authoring agent; only admitted verbs are callable per job.
|
|
|
2115
2173
|
## Gotchas
|
|
2116
2174
|
|
|
2117
2175
|
*TBD from real adopter friction; recorded here as they're found (repo-side friction goes
|
|
2118
|
-
to `docs/FINDINGS.md`).*
|
|
2176
|
+
to `docs/logs/FINDINGS.md`).*
|
|
2119
2177
|
|
|
2120
2178
|
## Constraints
|
|
2121
2179
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepublishOnly": "npm run build:types"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"bare-agent": "^0.
|
|
26
|
+
"bare-agent": "^0.39.0",
|
|
27
27
|
"bareguard": "^0.13.0",
|
|
28
28
|
"litectx": "^0.32.0"
|
|
29
29
|
},
|
package/src/behaviour.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// The run behaviour summary (PRD build-list §2, agreed 2026-08-23): ONE
|
|
2
|
+
// report-only text block computed entirely from a run's gate-audit JSONL.
|
|
3
|
+
// It gates NOTHING, mints no verdict, writes NOTHING to the spine — a pure
|
|
4
|
+
// read of records that already exist. "We can see what a run cost, never
|
|
5
|
+
// what it wasted" is the gap this closes; item 4's five-minute-replay test
|
|
6
|
+
// gets this as its cheap first slice.
|
|
7
|
+
//
|
|
8
|
+
// Gate-audit rows this reads (observed shape, this repo's real archive):
|
|
9
|
+
// {ts, seq, run_id, phase, action: {type, path?, args?}, decision, ...}
|
|
10
|
+
// `action.type` is one of `llm` (a provider round, NOT a tool call — always
|
|
11
|
+
// excluded), `read`, `edit`, `ctx_remember`. `action.args.tool` names the
|
|
12
|
+
// concrete tool for read-shaped actions (`shell_read`, `shell_grep`,
|
|
13
|
+
// `ctx_recall`, `ctx_get`, `ctx_recent`, `ctx_impact`, `ctx_related`);
|
|
14
|
+
// edit/ctx_remember rows carry no `args.tool`. `decision` is `allow` for a
|
|
15
|
+
// gated tool call, `null` for an llm row, or `deny`.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stable JSON — keys sorted at every level — so two calls with the same
|
|
19
|
+
* arguments in a different insertion order (or a different property visit
|
|
20
|
+
* order out of JSON.parse) hash to the same string. Used only to build the
|
|
21
|
+
* exact-repeat key below; it is not a general-purpose canonicalizer.
|
|
22
|
+
* @param {any} v
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function stableStringify(v) {
|
|
26
|
+
if (v === null || typeof v !== 'object') return JSON.stringify(v);
|
|
27
|
+
if (Array.isArray(v)) return `[${v.map(stableStringify).join(',')}]`;
|
|
28
|
+
const keys = Object.keys(v).sort();
|
|
29
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(v[k])}`).join(',')}}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The exact-repeat key for one tool-call action: `type` + `path` + a stable
|
|
34
|
+
* serialization of the WHOLE `args` object.
|
|
35
|
+
*
|
|
36
|
+
* Load-bearing, and deliberately not `{type, path}`: this repo's own history
|
|
37
|
+
* is a blind-instrument catalogue of collapsed keys reading two DIFFERENT
|
|
38
|
+
* things as the same thing (a ledger that summed input+output with no cache
|
|
39
|
+
* tiers, a gate audit that collapsed reads to `{type,path}`, `git status` as
|
|
40
|
+
* "did it write?"). If the gate ever starts recording byte ranges or offsets
|
|
41
|
+
* in `args`, two different slices of the same file must NOT count as a
|
|
42
|
+
* repeat — dropping `args` from the key would silently manufacture that
|
|
43
|
+
* exact confound here. This detector is exactly as sharp as what the gate
|
|
44
|
+
* records, no sharper: a tool that reads the same bytes through two
|
|
45
|
+
* differently-shaped argument objects will not be caught, and that is an
|
|
46
|
+
* honest limit, not a bug.
|
|
47
|
+
* @param {{type?: string, path?: string, args?: any}} action
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function repeatKey(action) {
|
|
51
|
+
return `${action?.type ?? ''} ${action?.path ?? ''} ${stableStringify(action?.args ?? null)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The display label for one tool-call breakdown row. Kept SEPARATE from the
|
|
56
|
+
* counting key (`args.tool || action.type`, computed inline in
|
|
57
|
+
* {@link runBehaviour}): the summary object stores the raw tool name so a
|
|
58
|
+
* consumer can trace a count back to its source, and only {@link
|
|
59
|
+
* formatBehaviour} shortens it for the printed line — the agreed shape reads
|
|
60
|
+
* "read"/"grep"/"recall", not "shell_read"/"shell_grep"/"ctx_recall".
|
|
61
|
+
* @param {string} key
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
function displayLabel(key) {
|
|
65
|
+
return key.replace(/^shell_/, '').replace(/^ctx_/, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Summarize what a run DID with its tools, from its gate-audit records
|
|
70
|
+
* alone. Pure and read-only: no IO, no spine writes, no verdict.
|
|
71
|
+
*
|
|
72
|
+
* Counting rules:
|
|
73
|
+
* - `llm` rows are provider rounds, never tool calls — always excluded.
|
|
74
|
+
* - a tool call counts toward `totalCalls`/`byTool`/`uniquePaths`/repeats
|
|
75
|
+
* only when `decision === 'allow'`; a `deny` is tallied separately in
|
|
76
|
+
* `denied` and touches nothing else (denied ≠ done).
|
|
77
|
+
* - `byTool` is keyed by `action.args.tool` when present, else
|
|
78
|
+
* `action.type` (an edit or ctx_remember has no `args.tool`).
|
|
79
|
+
* - exact repeats: see {@link repeatKey}. A first occurrence of an action
|
|
80
|
+
* is not itself a "repeat" — only the 2nd, 3rd, ... occurrences count,
|
|
81
|
+
* so `repeats` can never exceed `totalCalls - byTool-group-count`.
|
|
82
|
+
* @param {Array<{run_id?: string, action?: {type?: string, path?: string, args?: any}, decision?: string|null}>} events
|
|
83
|
+
* @param {{runId?: string}} [opts]
|
|
84
|
+
* @returns {{totalCalls: number, byTool: Record<string, number>, denied: number,
|
|
85
|
+
* uniquePaths: number, repeats: number, repeatPct: number|null}}
|
|
86
|
+
*/
|
|
87
|
+
export function runBehaviour(events, { runId } = {}) {
|
|
88
|
+
const list = Array.isArray(events) ? events : [];
|
|
89
|
+
const scoped = runId === undefined ? list : list.filter((e) => e?.run_id === runId);
|
|
90
|
+
|
|
91
|
+
const byTool = /** @type {Record<string, number>} */ ({});
|
|
92
|
+
const paths = new Set();
|
|
93
|
+
const keyCounts = new Map();
|
|
94
|
+
let totalCalls = 0;
|
|
95
|
+
let denied = 0;
|
|
96
|
+
|
|
97
|
+
for (const e of scoped) {
|
|
98
|
+
const action = e?.action ?? {};
|
|
99
|
+
if (action.type === 'llm') continue; // a provider round, never a tool call
|
|
100
|
+
if (e?.decision === 'deny') { denied += 1; continue; }
|
|
101
|
+
if (e?.decision !== 'allow') continue; // neither allow nor deny: not a counted call
|
|
102
|
+
|
|
103
|
+
totalCalls += 1;
|
|
104
|
+
const tool = action.args?.tool ?? action.type ?? 'unknown';
|
|
105
|
+
byTool[tool] = (byTool[tool] ?? 0) + 1;
|
|
106
|
+
if (action.path) paths.add(action.path);
|
|
107
|
+
|
|
108
|
+
const k = repeatKey(action);
|
|
109
|
+
keyCounts.set(k, (keyCounts.get(k) ?? 0) + 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let repeats = 0;
|
|
113
|
+
for (const count of keyCounts.values()) if (count > 1) repeats += count - 1;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
totalCalls,
|
|
117
|
+
byTool,
|
|
118
|
+
denied,
|
|
119
|
+
uniquePaths: paths.size,
|
|
120
|
+
repeats,
|
|
121
|
+
repeatPct: totalCalls > 0 ? Math.round((repeats / totalCalls) * 100) : null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render a {@link runBehaviour} summary as the agreed printable block
|
|
127
|
+
* (2–4 short lines). A zero-call run formats plainly ("0 tool calls") rather
|
|
128
|
+
* than a NaN-laced percentage — an empty run is a complete, reportable
|
|
129
|
+
* answer, not a division error.
|
|
130
|
+
* @param {ReturnType<typeof runBehaviour>} summary
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
export function formatBehaviour(summary) {
|
|
134
|
+
const { totalCalls, byTool, denied, repeats, repeatPct } = summary; // uniquePaths: data-only, not in the agreed printed shape
|
|
135
|
+
const lines = [];
|
|
136
|
+
|
|
137
|
+
if (totalCalls === 0) {
|
|
138
|
+
lines.push('0 tool calls');
|
|
139
|
+
} else {
|
|
140
|
+
const breakdown = Object.entries(byTool)
|
|
141
|
+
.sort((a, b) => b[1] - a[1])
|
|
142
|
+
.map(([tool, n]) => `${n} ${displayLabel(tool)}`)
|
|
143
|
+
.join(', ');
|
|
144
|
+
lines.push(`${totalCalls} tool call${totalCalls === 1 ? '' : 's'} · ${breakdown}`);
|
|
145
|
+
lines.push(`${repeats} exact repeat${repeats === 1 ? '' : 's'} (~${repeatPct}%)`);
|
|
146
|
+
}
|
|
147
|
+
if (denied > 0) lines.push(`${denied} denied`);
|
|
148
|
+
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
package/src/bridges.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer 3 — the REUSE registry (design record:
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/product/2026-08-01-layer-3-reuse-design.md, D1/D2/D6, frozen 2026-08-01).
|
|
3
3
|
//
|
|
4
4
|
// A BRIDGE is the plan a green actually executed, kept so the next run of the same
|
|
5
5
|
// SHAPE starts from it instead of cold. This module is the box and the label on it:
|
package/src/calibrate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// THE CALIBRATION GATE — softgreen module 5 (design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/product/2026-08-17-softgreen-review-door-design.md §4.4, the
|
|
3
3
|
// 2026-08-18 threshold addendum rulings 1–3, and the second same-day addendum's
|
|
4
4
|
// rulings 1 and 3).
|
|
5
5
|
//
|
package/src/cardauthor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// THE TWO SIGNED ARTIFACTS — softgreen module 4 (design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/product/2026-08-17-softgreen-review-door-design.md §4.3 and §4.4,
|
|
3
3
|
// plus the 2026-08-18 threshold addendum).
|
|
4
4
|
//
|
|
5
5
|
// Module 1 built the judged floor's core, module 2 wired the stage into a close,
|
package/src/index.js
CHANGED
|
@@ -228,6 +228,11 @@ export {
|
|
|
228
228
|
HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED, HUMAN_CHECKPOINTS,
|
|
229
229
|
} from './declaredclose.js';
|
|
230
230
|
export { closeStagesOf } from './plan.js';
|
|
231
|
+
// Build-list item 2 (2026-08-23) — the run behaviour summary: a report-only
|
|
232
|
+
// read of a run's gate-audit records. Gates nothing, mints no verdict,
|
|
233
|
+
// writes nothing to the spine; `runBehaviour` computes, `formatBehaviour`
|
|
234
|
+
// renders the printable block.
|
|
235
|
+
export { runBehaviour, formatBehaviour } from './behaviour.js';
|
|
231
236
|
// M4b — the interview, the refusal, the composition, and D9's three gates.
|
|
232
237
|
// `prepareSigning` returns the resolved spec's HASH and the seed evidence; it
|
|
233
238
|
// never signs — the approvals array and the human's word are unchanged.
|
package/src/job.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Job-spec validator — the OPERATOR-owned half of the config story (N1,
|
|
2
|
-
// design record: docs/
|
|
2
|
+
// design record: docs/product/2026-07-12-n1-job-close-schema-design.md).
|
|
3
3
|
// A job spec is the arbiter's rulebook: the close chain, the budget, the
|
|
4
4
|
// outer write fence, the escalation route. It is pure declarative data —
|
|
5
5
|
// no freeform code is expressible anywhere in it — and it validates
|
package/src/judged.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// THE JUDGED FLOOR'S CORE — softgreen module 1 (design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/product/2026-08-17-softgreen-review-door-design.md §4, plus the
|
|
3
3
|
// 2026-08-18 threshold addendum). POC: poc/softgreen-judge/poc.mjs — validated
|
|
4
4
|
// logic, rewritten here, never copied (graduation is a rewrite).
|
|
5
5
|
//
|
package/src/ledger.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The upstream ledger (module 4) — reactive lib/primitive incident monitoring.
|
|
2
|
-
// Design record: docs/
|
|
2
|
+
// Design record: docs/product/2026-07-11-upstream-ledger-design.md (adaptlearn
|
|
3
3
|
// POC'd spec) + its 2026-07-13 addendum: the bareloop event mapping. One
|
|
4
4
|
// append-only JSONL both the consumer (the panel, N6) and the maintainer read;
|
|
5
5
|
// the A1/A2/A3 upstream-ask flow, mechanized — evidence in, human judgment out.
|
package/src/planrun.js
CHANGED
|
@@ -42,9 +42,101 @@ import {
|
|
|
42
42
|
REVIEW_DOOR, doorOpens, mechanicalStages,
|
|
43
43
|
} from './kinds.js';
|
|
44
44
|
import { workBranchName, prepareWorkBranch } from './workbranch.js';
|
|
45
|
+
import { TRANSPORT_MAX_ATTEMPTS, isTransportFailure } from './transport.js';
|
|
45
46
|
|
|
46
47
|
const require = createRequire(import.meta.url);
|
|
47
|
-
const { Loop, wireGate, HaltError } = require('bare-agent');
|
|
48
|
+
const { Loop, Retry, wireGate, HaltError } = require('bare-agent');
|
|
49
|
+
|
|
50
|
+
// F115 — hamr's ruling verbatim: "one retry for transport layer failure and
|
|
51
|
+
// reporting with the rest end of gate." TRANSPORT_MAX_ATTEMPTS is fixed
|
|
52
|
+
// (src/transport.js — never a job-spec/argv knob); `retryOn` is bare-agent's
|
|
53
|
+
// own extension seam (node_modules/bare-agent/src/retry.js), constrained to
|
|
54
|
+
// ONLY the no-HTTP-response transport class so a provider RESPONSE (4xx/5xx/
|
|
55
|
+
// 429) still rides bare-agent's own unchanged policy. One instance: `Retry`
|
|
56
|
+
// carries no per-call state, so it is safe to share across every worker.
|
|
57
|
+
// timeout:0 disables Retry's own per-attempt race (this run already has its
|
|
58
|
+
// own timing authority — the stall watchdog and the wall clock, src/stall.js
|
|
59
|
+
// / src/clock.js — and a second racing 60s clock here would just be a second,
|
|
60
|
+
// disagreeing one; bare-agent's own BA-18 idle-socket timeout is what
|
|
61
|
+
// actually bounds a hung call, and that is exactly one of the errors this
|
|
62
|
+
// retry is FOR). `backoff` is deliberately left unset: bare-agent's
|
|
63
|
+
// constructor does `options.backoff || 'exponential'`, so a literal `0` is
|
|
64
|
+
// falsy and would silently fall through to the same exponential default
|
|
65
|
+
// anyway — passing it would read as "no pause" while doing the opposite.
|
|
66
|
+
// With `maxAttempts:2` there is only ever ONE delay to compute (attempt 1),
|
|
67
|
+
// so the default exponential backoff's own formula (`2**0 * 1000`, capped at
|
|
68
|
+
// 30s) is a flat ~1s pause before the one retry — named here, not hidden
|
|
69
|
+
// behind a knob that lies about what it does.
|
|
70
|
+
const transportRetry = new Retry({ maxAttempts: TRANSPORT_MAX_ATTEMPTS, retryOn: isTransportFailure, timeout: 0 });
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Wrap a Loop provider's `generate` so a single transport-class throw (fetch
|
|
74
|
+
* itself throwing — no HTTP response was ever produced) gets ONE retry via
|
|
75
|
+
* `transportRetry`, and the outcome is reported once the retry is settled.
|
|
76
|
+
* Every other property of `baseProvider` (`.name`, `.model`, `.ownsCycle`,
|
|
77
|
+
* `.policy`) passes through unchanged — bare-agent's Loop reads those
|
|
78
|
+
* directly (src/loop.js).
|
|
79
|
+
*
|
|
80
|
+
* F64 carve-out: a wall-DERIVED timeout (bare-agent's own `ETIMEDOUT`/
|
|
81
|
+
* `EDEADLINE`, thrown because `clock.callTimeoutMs()` told the provider to
|
|
82
|
+
* cut the call — `isWallTimeout`, src/clock.js) is doctrine's governance
|
|
83
|
+
* stop, never a transport casualty, and retrying it would both mis-spend a
|
|
84
|
+
* round the operator's clock already ended and corrupt `categorize`'s own
|
|
85
|
+
* classification (which reads the SAME error a moment later). So the
|
|
86
|
+
* per-call `retryOn` here is `isTransportFailure(err) && !isWallTimeout(err,
|
|
87
|
+
* clock)` — the clock's verdict always wins over the transport shape.
|
|
88
|
+
*
|
|
89
|
+
* Scope note: this wraps the ONE seam that builds every worker Loop
|
|
90
|
+
* (scout/drafter/step-worker/fix-worker, planrun.js `newLoop` ~line 2240) —
|
|
91
|
+
* and ONLY its Loop-driven (anthropic-api-shaped) provider branch. The judge
|
|
92
|
+
* loops (src/judged.js), the native/CLI session path (this file, `mkWorker`'s
|
|
93
|
+
* NATIVE branch above `ask`/`askFrom`, AND the toolless native/claude-json
|
|
94
|
+
* branch of `newLoop`'s own `loopProvider` — a retried native session
|
|
95
|
+
* re-spends a subscription turn, a different failure/cost model entirely),
|
|
96
|
+
* and the authoring scout (src/authorscout.js) are each a distinct call site
|
|
97
|
+
* outside hamr's ruling and are deliberately left unwrapped.
|
|
98
|
+
* @param {any} baseProvider
|
|
99
|
+
* @param {string} phase
|
|
100
|
+
* @param {(type: string, data?: object) => any} emit
|
|
101
|
+
* @param {(s: string) => string} scrub
|
|
102
|
+
* @param {import('./clock.js').Clock} clock
|
|
103
|
+
*/
|
|
104
|
+
function withTransportRetry(baseProvider, phase, emit, scrub, clock) {
|
|
105
|
+
return new Proxy(baseProvider, {
|
|
106
|
+
get(target, prop, receiver) {
|
|
107
|
+
if (prop !== 'generate') return Reflect.get(target, prop, receiver);
|
|
108
|
+
return async (...args) => {
|
|
109
|
+
let attempts = 0;
|
|
110
|
+
let firstErr = null;
|
|
111
|
+
const fn = async () => {
|
|
112
|
+
attempts += 1;
|
|
113
|
+
try {
|
|
114
|
+
return await target.generate(...args);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (attempts === 1) firstErr = err;
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
// `recovered` is knowable only once the retried attempt has settled
|
|
121
|
+
// (succeeded or exhausted the budget) — emit exactly once, here,
|
|
122
|
+
// after the outcome is known, never speculatively before it.
|
|
123
|
+
const report = (recovered) => {
|
|
124
|
+
if (attempts <= 1) return; // no retry happened — nothing to report
|
|
125
|
+
const message = String(firstErr?.message ?? firstErr ?? '').slice(0, 200);
|
|
126
|
+
emit('transport-retry', { phase, attempt: 1, error: scrub(message), recovered });
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
const result = await transportRetry.call(fn, { retryOn: (err) => isTransportFailure(err) && !isWallTimeout(err, clock) });
|
|
130
|
+
report(true);
|
|
131
|
+
return result;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
report(false);
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
48
140
|
const { createShellTools } = require('bare-agent/tools');
|
|
49
141
|
|
|
50
142
|
/** the scout's hard round bound — read-only survey, never a worker (v1.12 §1) */
|
|
@@ -714,6 +806,14 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
714
806
|
* 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
715
807
|
*/
|
|
716
808
|
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, maxStepRounds = 40, layerRoot = false, readShim = false, 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 }) {
|
|
809
|
+
// MEMORY-CACHE: what the read shim (src/readshim.js) saved THIS run, summed across
|
|
810
|
+
// every mkWorker's own shim instance (scout, drafter, each step's worker, the fix
|
|
811
|
+
// worker) — one accumulator closed over by all of them, because the shim's ledger
|
|
812
|
+
// is deliberately per-worker (F-class reset boundary) while this readout is
|
|
813
|
+
// per-run. `wrapRead`'s call sites feed it via `onCount`; nothing here re-derives
|
|
814
|
+
// a count, it only sums what the shim already measured. Read, never written, by
|
|
815
|
+
// anything outside the `finally` below.
|
|
816
|
+
const memoryCacheCounts = { pointered: 0, capped: 0, bytesWithheld: 0 };
|
|
717
817
|
workdir = resolve(workdir);
|
|
718
818
|
// The read shim's ARM, resolved at the door — before the workdir is read, before
|
|
719
819
|
// the scout, before a single token. An unrecognised spelling throws HERE, where
|
|
@@ -744,6 +844,12 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
744
844
|
throw new TypeError(`readShim: this arm caps reads, so a step granting "read" must also grant ${RETRIEVAL_PAIR.join(' and ')} — but the signed ceiling [${signedCeiling.join(', ')}] offers no ${short.join('/')}, so no legal plan exists and every draft would red as "read-blind". Re-sign the spec with ${short.join(' and ')}, or run with the shim off.`);
|
|
745
845
|
}
|
|
746
846
|
}
|
|
847
|
+
// MEMORY-CACHE's own `try`: everything from here on is a run that actually
|
|
848
|
+
// BEGAN (both $0 refusals above — the unknown-arm throw and the G1 throw —
|
|
849
|
+
// already happened and never touch this block), so `finally` below is the
|
|
850
|
+
// one place the accumulated counts are read out, no matter which of the
|
|
851
|
+
// paths below this point returns or throws.
|
|
852
|
+
try {
|
|
747
853
|
// ONE spelling of the redaction, housed next to the inventory it reads
|
|
748
854
|
// (src/validate.js) — the same helper the isolate verbs scrub the litectx store
|
|
749
855
|
// with. Two hand-spelled copies of "what a secret looks like" is exactly how
|
|
@@ -1961,7 +2067,16 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1961
2067
|
// are one shim because the cap creates the second problem — its strategy line
|
|
1962
2068
|
// sends a capped worker to ctx_recall/ctx_get for a whole function, and a stale
|
|
1963
2069
|
// pointer there is the cap's own dead end, not litectx's.
|
|
1964
|
-
const shim = createReadShim({
|
|
2070
|
+
const shim = createReadShim({
|
|
2071
|
+
arm: shimArm,
|
|
2072
|
+
// MEMORY-CACHE: this worker's shim reports into the RUN-level accumulator
|
|
2073
|
+
// declared at the top of `runPlan` — one shim per worker, one sum per run.
|
|
2074
|
+
onCount: (kind, bytes) => {
|
|
2075
|
+
if (kind === 'pointered') memoryCacheCounts.pointered++;
|
|
2076
|
+
else memoryCacheCounts.capped++;
|
|
2077
|
+
memoryCacheCounts.bytesWithheld += bytes;
|
|
2078
|
+
},
|
|
2079
|
+
});
|
|
1965
2080
|
const rdTool = shell.find((/** @type {{name: string}} */ t) => t.name === TOOL_BY_VERB.read);
|
|
1966
2081
|
// F48: on native, bound shell_read below the CLI's tool-result display cap and hand back a
|
|
1967
2082
|
// TRUSTED truncation notice steering to ctx_get — the CLI's own truncation blinds the worker
|
|
@@ -2111,9 +2226,12 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2111
2226
|
// claude-json TEXT path reports a real per-call cost that the Loop's
|
|
2112
2227
|
// onLlmResult meters exactly like an API round, so the drafter's spend is
|
|
2113
2228
|
// never invisible (F6/F44). The gate policy is wired but idle (no tools).
|
|
2229
|
+
// F115's retry wraps ONLY the API-shaped branch — a native/claude-json session
|
|
2230
|
+
// (below) has its own failure and cost model (a retried native session
|
|
2231
|
+
// re-spends a subscription turn, not a metered fetch) and is out of scope.
|
|
2114
2232
|
const loopProvider = native
|
|
2115
2233
|
? /** @type {any} */ (nativeProvider)({ policy, maxTurns, hasTools: false })
|
|
2116
|
-
: (workerProvider ?? provider);
|
|
2234
|
+
: withTransportRetry(workerProvider ?? provider, phase, emit, scrub, clock);
|
|
2117
2235
|
// F66 — the stall watchdog. bare-agent's `timeoutMs` bounds socket INACTIVITY,
|
|
2118
2236
|
// not call duration (provider-http.js `req.setTimeout` resets on every byte),
|
|
2119
2237
|
// so a connection that stays alive while producing nothing is invisible to it:
|
|
@@ -3490,4 +3608,23 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3490
3608
|
// the verdict was arrived at.
|
|
3491
3609
|
if (fixOutcome === 'green') await emitReviewDoor('green', lastCloseVerdict);
|
|
3492
3610
|
return fixOutcome === 'green' ? 'green' : 'escalated';
|
|
3611
|
+
} finally {
|
|
3612
|
+
// Emitted on EVERY exit (return or throw) — `finally` fires exactly once,
|
|
3613
|
+
// right before this call returns to `runJob`, which is where every other
|
|
3614
|
+
// caller's `job-end` lands next. Armed-only: an unarmed run leaves no trace
|
|
3615
|
+
// (absence reported as absence, never a fabricated zero line, PRD antigen).
|
|
3616
|
+
// No cost fields — this is not a spend record and spend-slicing instruments
|
|
3617
|
+
// are unaffected by it (F6/F12's ACCOUNTED_ROUND_TYPES list is untouched).
|
|
3618
|
+
if (readShimArm(readShim).on) {
|
|
3619
|
+
emit('memory-cache', {
|
|
3620
|
+
pointered: memoryCacheCounts.pointered,
|
|
3621
|
+
capped: memoryCacheCounts.capped,
|
|
3622
|
+
bytesWithheld: memoryCacheCounts.bytesWithheld,
|
|
3623
|
+
// bytes→tokens is an ESTIMATE (÷4, the same rough conversion used
|
|
3624
|
+
// elsewhere) — the field is named `approx…` so no consumer reads it
|
|
3625
|
+
// as measured the way `bytesWithheld` itself is.
|
|
3626
|
+
approxTokens: Math.round(memoryCacheCounts.bytesWithheld / 4),
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3493
3630
|
}
|
package/src/readshim.js
CHANGED
|
@@ -332,13 +332,18 @@ const stalePastEofNotice = (path, startLine, endLine, lines, detail) =>
|
|
|
332
332
|
* reference to the ledger — see its own note. Both live in one object because
|
|
333
333
|
* they are one worker's shim: the read seam and the retrieval seam the cap
|
|
334
334
|
* steers a capped worker towards.
|
|
335
|
-
* @param {{cap?: number, arm?: ReadShimArm
|
|
336
|
-
* shipped value is `READ_SHIM_CAP`; the option
|
|
337
|
-
* two bounds to one number rather than two
|
|
338
|
-
* levers are live (`readShimArm`). Defaults to
|
|
339
|
-
* before the arms existed keeps the behaviour it
|
|
335
|
+
* @param {{cap?: number, arm?: ReadShimArm, onCount?: ((kind: 'pointered'|'capped', bytesWithheld: number) => void)|null}} [opts]
|
|
336
|
+
* `cap`: delivery bound in bytes (the shipped value is `READ_SHIM_CAP`; the option
|
|
337
|
+
* exists so the native path can pin the two bounds to one number rather than two
|
|
338
|
+
* constants drifting apart). `arm`: which levers are live (`readShimArm`). Defaults to
|
|
339
|
+
* ALL of them, so every caller written before the arms existed keeps the behaviour it
|
|
340
|
+
* was written against. `onCount`: MEMORY-CACHE — fired once per pointer/capped-slice
|
|
341
|
+
* response with exactly the bytes withheld on THAT response, for a caller (e.g.
|
|
342
|
+
* `runPlan`) that sums several workers' shims into one run-level readout. Never called
|
|
343
|
+
* for a diff response (L2 is its own lever, uncounted here) or a first read under the
|
|
344
|
+
* cap (nothing was withheld).
|
|
340
345
|
*/
|
|
341
|
-
export function createReadShim({ cap = READ_SHIM_CAP, arm = ARM_ALL } = {}) {
|
|
346
|
+
export function createReadShim({ cap = READ_SHIM_CAP, arm = ARM_ALL, onCount = null } = {}) {
|
|
342
347
|
/** path → what the worker has been handed of the CURRENT content. `full` is
|
|
343
348
|
* the delivered text itself, kept ONLY while the worker holds the whole file
|
|
344
349
|
* (null otherwise) because that is the only state a diff is legal from.
|
|
@@ -354,6 +359,22 @@ export function createReadShim({ cap = READ_SHIM_CAP, arm = ARM_ALL } = {}) {
|
|
|
354
359
|
* @type {Map<string, {hash: string, total: number, delivered: number, full: string|null}>} */
|
|
355
360
|
const ledger = new Map();
|
|
356
361
|
|
|
362
|
+
// MEMORY-CACHE counters (this worker's own ledger, summed by a caller across
|
|
363
|
+
// workers for a run-level readout — see `onCount` above). Counted only where
|
|
364
|
+
// it is EXACT, never estimated: a pointer withholds exactly `total` bytes (the
|
|
365
|
+
// whole delivery it replaces) and a capped slice withholds exactly `total - end`
|
|
366
|
+
// (the tail this call did not send) — both numbers the ledger already computes
|
|
367
|
+
// for its own bookkeeping below, read here rather than re-derived.
|
|
368
|
+
let pointered = 0;
|
|
369
|
+
let capped = 0;
|
|
370
|
+
let bytesWithheld = 0;
|
|
371
|
+
/** @param {'pointered'|'capped'} kind @param {number} bytes */
|
|
372
|
+
const count = (kind, bytes) => {
|
|
373
|
+
if (kind === 'pointered') pointered++; else capped++;
|
|
374
|
+
bytesWithheld += bytes;
|
|
375
|
+
if (onCount) onCount(kind, bytes);
|
|
376
|
+
};
|
|
377
|
+
|
|
357
378
|
/**
|
|
358
379
|
* THE STALE-POINTER SERVE. When `ctx_get` throws `StalePointerError` — the file
|
|
359
380
|
* changed on disk after indexing — hand back the REQUESTED LINE RANGE read
|
|
@@ -510,6 +531,9 @@ export function createReadShim({ cap = READ_SHIM_CAP, arm = ARM_ALL } = {}) {
|
|
|
510
531
|
// slice path and is delivered (as nothing) truthfully.
|
|
511
532
|
if (arm.pointer && seen && seen.hash === hash && start >= total) {
|
|
512
533
|
ledger.set(path, { hash, total, delivered: total, full: r });
|
|
534
|
+
// Withheld = the whole delivery this pointer replaces: `total` bytes the
|
|
535
|
+
// worker asked to be re-sent and was not, because it already holds them.
|
|
536
|
+
count('pointered', total);
|
|
513
537
|
return pointer(path, total);
|
|
514
538
|
}
|
|
515
539
|
ledger.set(path, { hash, total, delivered: end, full: end === total ? r : null });
|
|
@@ -518,12 +542,20 @@ export function createReadShim({ cap = READ_SHIM_CAP, arm = ARM_ALL } = {}) {
|
|
|
518
542
|
// no notice, no reframing. That is the overwhelming majority of reads, and
|
|
519
543
|
// it keeps the shim invisible where it buys nothing.
|
|
520
544
|
if (start === 0 && end === total) return slice;
|
|
545
|
+
// Withheld = the tail beyond this slice (`total - end`), not the slice
|
|
546
|
+
// itself — the slice IS what was sent; the remainder is what was not.
|
|
547
|
+
if (end < total) count('capped', total - end);
|
|
521
548
|
return slice + (end < total ? capNotice(start, end, total) : doneNotice(start, total));
|
|
522
549
|
};
|
|
523
550
|
return tool;
|
|
524
551
|
};
|
|
525
552
|
|
|
526
|
-
|
|
553
|
+
/** MEMORY-CACHE: this worker's own counts, exact — never re-derived, never
|
|
554
|
+
* estimated. `bytesWithheld` sums only the `pointered`/`capped` cases above;
|
|
555
|
+
* the diff lever (L2) is a different response shape and is not counted here. */
|
|
556
|
+
const summary = () => ({ pointered, capped, bytesWithheld });
|
|
557
|
+
|
|
558
|
+
return { arm, cap, wrapRead, serveStale, summary };
|
|
527
559
|
}
|
|
528
560
|
|
|
529
561
|
/**
|
package/src/reuse.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer 3 — the D7 ENVELOPE and the REUSE RUNNER (design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/product/2026-08-01-layer-3-reuse-design.md, D3/D5/D6/D7 + R1).
|
|
3
3
|
//
|
|
4
4
|
// hamr's sentence is the whole specification of this file, verbatim:
|
|
5
5
|
//
|
|
@@ -115,8 +115,30 @@ export const REUSE_GRADED_RED = Object.freeze(['escalated']);
|
|
|
115
115
|
* how they would come to disagree about which member is which: the first three are
|
|
116
116
|
* allowances the loop's remaining tries are meant to spend against, and the human
|
|
117
117
|
* ones are the stop no further try can change (see `hardStop`).
|
|
118
|
+
*
|
|
119
|
+
* `provider-red` folded in 2026-08-25 (PRD v1.80 TODO #4, F115 "Ruled: one transport
|
|
120
|
+
* retry"; hamr's ruling verbatim: *"joins resume anyways and you decide but you are
|
|
121
|
+
* given an honest readout and with 1 retry"*). It is a MACHINE halt, not a human one
|
|
122
|
+
* — it does NOT go into `HUMAN_CHECKPOINTS` — but it is a checkpoint by the same
|
|
123
|
+
* test every other member here passes: the close never got the say it was asked
|
|
124
|
+
* for (no verdict was rendered, so it is not a graded red — see `REUSE_GRADED_RED`
|
|
125
|
+
* above), and the scout+draft and every finished step it bought stand on disk
|
|
126
|
+
* exactly like a cap-halt or a wall-halt. The one transport retry
|
|
127
|
+
* (`src/planrun.js`'s `withTransportRetry`) already covers the common case; when
|
|
128
|
+
* that retry ALSO fails, throwing the run away because the transport hiccuped
|
|
129
|
+
* twice is throwing away real work for no reason a machine can judge. Deliberately
|
|
130
|
+
* NO cost/step threshold gates this: a threshold is a number picked from a small
|
|
131
|
+
* observed sample (F115 logged exactly 7 rows), which is arbiter territory
|
|
132
|
+
* reserved for hamr — the operator decides case by case, every time, off the
|
|
133
|
+
* honest readout `scripts/run-u.mjs` prints.
|
|
134
|
+
*
|
|
135
|
+
* This entry was built 2026-08-24 as a LOCAL widening in `scripts/run-u.mjs`
|
|
136
|
+
* (`RESUMABLE_HALTS = [...CHECKPOINT_OUTCOMES, 'provider-red']`) because this file
|
|
137
|
+
* sat outside that pass's scope — the fold here is the follow-up, so this repo
|
|
138
|
+
* does not end up with two lists that can drift, which is the exact failure this
|
|
139
|
+
* constant exists to end (see the paragraph above).
|
|
118
140
|
*/
|
|
119
|
-
export const CHECKPOINT_OUTCOMES = Object.freeze(['cap-halt', 'wall-halt', 'step-stalled', ...HUMAN_CHECKPOINTS]);
|
|
141
|
+
export const CHECKPOINT_OUTCOMES = Object.freeze(['cap-halt', 'wall-halt', 'step-stalled', 'provider-red', ...HUMAN_CHECKPOINTS]);
|
|
120
142
|
|
|
121
143
|
/**
|
|
122
144
|
* v1's definition of "the same KIND of recipe" — and it is the LOAD GATE's own: the
|
package/src/root.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer R — the root: the within-run ratchet (LAYERS.md item 2; design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/product/2026-07-19-layer-r-design.md). One state that survives attempts
|
|
3
3
|
// inside ONE run so a never-green run stops repeating itself (F21). Role is
|
|
4
4
|
// CONTINUITY ONLY — F39 measured that hand-delivered state buys no conversion;
|
|
5
5
|
// the semantic converter is Layer 2's job, not this module's.
|
package/src/run.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// smoke, the ONE budget ledger (cap-not-estimate; unpriced is never free, F6),
|
|
4
4
|
// and the decision-ready stops. It composes the plan flow below it and
|
|
5
5
|
// interprets nothing itself.
|
|
6
|
-
// Design record: docs/
|
|
6
|
+
// Design record: docs/product/2026-07-12-n2-headless-loop-design.md.
|
|
7
7
|
//
|
|
8
8
|
// The legacy operator-authored `steps[]` path was DELETED 2026-07-26 (PRD
|
|
9
9
|
// v1.32): the drafting call, config-v1, the per-step interpret loops and the
|
|
@@ -254,6 +254,15 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
254
254
|
// outcome — keying on the outcome would floor the exact stop too, which is the same
|
|
255
255
|
// dishonesty pointing the other way.
|
|
256
256
|
let cutMidCall = false;
|
|
257
|
+
// F115 — did any worker call in this run absorb a transport-class retry
|
|
258
|
+
// (hamr's ruling: one retry, transport-only, planrun.js `withTransportRetry`)?
|
|
259
|
+
// The FIRST attempt of a retried call may already have been billed and threw
|
|
260
|
+
// before returning any usage figure — the same unknown a self-healed stall
|
|
261
|
+
// and a mid-call wall cut carry, so the same one-way floor applies even when
|
|
262
|
+
// the retry recovered and the run finishes green. Read off the `transport-retry`
|
|
263
|
+
// spine record's presence alone, never its `recovered` field: a recovered
|
|
264
|
+
// retry still hides the first attempt's possible spend.
|
|
265
|
+
let transportRetried = false;
|
|
257
266
|
// The money on the terminal record, and whether the money is EXACT. `spentUsd`
|
|
258
267
|
// is the accumulated sum of PRICED rounds ONLY — never an estimate derived
|
|
259
268
|
// from tokens or averages (cap-not-estimate). When any round came back
|
|
@@ -263,7 +272,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
263
272
|
// branch on field presence.
|
|
264
273
|
// ONE spelling of the four causes, so the terminal and the in-flight money readout can
|
|
265
274
|
// never disagree about whether the same run's figure is exact.
|
|
266
|
-
const spendComplete = () => !unpriced && !stalled && !cutMidCall && !priorFloor;
|
|
275
|
+
const spendComplete = () => !unpriced && !stalled && !cutMidCall && !priorFloor && !transportRetried;
|
|
267
276
|
const spend = () => ({
|
|
268
277
|
spentUsd,
|
|
269
278
|
spendComplete: spendComplete(),
|
|
@@ -364,6 +373,10 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
364
373
|
// One-way, and read by `spend()` — the run ends on this record either way, but the
|
|
365
374
|
// between-rounds stop must keep its exact figure rather than inheriting a floor.
|
|
366
375
|
if (type === 'wall-halt' && /** @type {any} */ (data)?.cutMidCall === true) cutMidCall = true;
|
|
376
|
+
// F115 — one-way, same lane as `stall`/`cutMidCall`: a transport retry's
|
|
377
|
+
// FIRST attempt threw before any usage figure came back, so the priced sum
|
|
378
|
+
// is a floor for the rest of the run regardless of `recovered`.
|
|
379
|
+
if (type === 'transport-retry') transportRetried = true;
|
|
367
380
|
return emit(type, data);
|
|
368
381
|
};
|
|
369
382
|
const pricingRed = () => {
|
package/src/selection.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer 3 — D3's DISPLAY half: the workflow LISTING, and the prompt that asks for a
|
|
2
|
-
// pick (design record docs/
|
|
2
|
+
// pick (design record docs/product/2026-08-01-layer-3-reuse-design.md, D3).
|
|
3
3
|
//
|
|
4
4
|
// hamr's ruling, verbatim: *"i think there should be a listing for workflows names that
|
|
5
5
|
// user can choose or auto-choose, or do we want llm to always decide (we still need to
|
package/src/transport.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// hamr's ruling, verbatim: "one retry for transport layer failure and
|
|
2
|
+
// reporting with the rest end of gate." (F115, docs/logs/FINDINGS.md.)
|
|
3
|
+
//
|
|
4
|
+
// A transport failure is fetch THROWING — the request never produced an HTTP
|
|
5
|
+
// response at all (a TLS fault mid-record, a reset socket, a DNS/network
|
|
6
|
+
// failure). That is a DIFFERENT unknown from an HTTP response the provider
|
|
7
|
+
// DID send (4xx/5xx/429): a response means the provider answered, so
|
|
8
|
+
// bare-agent's own retry policy for those stays exactly what it is — this
|
|
9
|
+
// module never widens that. Retrying only the no-response case is the whole
|
|
10
|
+
// point: a call that dies mid-read may already have been billed (retry can
|
|
11
|
+
// pay twice), so the retry budget stays as tight as hamr authorized it —
|
|
12
|
+
// exactly one extra attempt, never configurable upward from a job spec or
|
|
13
|
+
// argv (tighten-only doctrine).
|
|
14
|
+
//
|
|
15
|
+
// Scope: this module wires ONE seam — the worker Loop built in
|
|
16
|
+
// src/planrun.js's `newLoop` (~line 2239), which is the ONE factory behind
|
|
17
|
+
// every scout/drafter/step/fix-worker call. It deliberately does NOT reach
|
|
18
|
+
// the judge loops (src/judged.js), the native/CLI loop (src/planrun.js
|
|
19
|
+
// ~2089), or the authoring scout (src/authorscout.js) — none of those are in
|
|
20
|
+
// scope of hamr's ruling and each has its own transport story.
|
|
21
|
+
|
|
22
|
+
/** The fixed retry budget: ONE extra attempt after the first transport
|
|
23
|
+
* throw. Never read from a job spec or CLI flag — raising it is arbiter
|
|
24
|
+
* territory (only a code change hamr signs, never a run-time knob). */
|
|
25
|
+
export const TRANSPORT_RETRIES = 1;
|
|
26
|
+
|
|
27
|
+
/** bare-agent's `Retry.call` counts ATTEMPTS, not retries. */
|
|
28
|
+
export const TRANSPORT_MAX_ATTEMPTS = TRANSPORT_RETRIES + 1;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Is `err` a transport-class failure — the request never produced an HTTP
|
|
32
|
+
* response? True for: a TLS fault mid-record ("bad record mac" and kin), a
|
|
33
|
+
* reset/broken/timed-out socket (ECONNRESET, EPIPE, ETIMEDOUT, ECONNREFUSED,
|
|
34
|
+
* ENETUNREACH — by code on the error itself or on `err.cause`), and
|
|
35
|
+
* `fetch failed` wrapping a network `cause`. False for anything carrying an
|
|
36
|
+
* HTTP status (`err.status`/`err.statusCode`, 4xx/5xx/429) or an explicit
|
|
37
|
+
* `err.retryable === false` — those are provider RESPONSES, not transport
|
|
38
|
+
* casualties, and bare-agent's own policy governs them unchanged.
|
|
39
|
+
* @param {any} err
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function isTransportFailure(err) {
|
|
43
|
+
if (err == null) return false;
|
|
44
|
+
// An explicit HTTP response is never a transport casualty, no matter what
|
|
45
|
+
// its message says — the provider answered.
|
|
46
|
+
const status = err.status ?? err.statusCode;
|
|
47
|
+
if (typeof status === 'number') return false;
|
|
48
|
+
if (err.retryable === false) return false;
|
|
49
|
+
|
|
50
|
+
const codeOf = (e) => (e && typeof e.code === 'string' ? e.code : null);
|
|
51
|
+
const TRANSPORT_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'ECONNREFUSED', 'ENETUNREACH', 'EAI_AGAIN']);
|
|
52
|
+
const code = codeOf(err) ?? codeOf(err.cause);
|
|
53
|
+
if (code && TRANSPORT_CODES.has(code)) return true;
|
|
54
|
+
|
|
55
|
+
const message = String(err.message ?? err ?? '');
|
|
56
|
+
if (/bad record mac/i.test(message)) return true;
|
|
57
|
+
if (TRANSPORT_CODES.has(message.trim())) return true;
|
|
58
|
+
for (const c of TRANSPORT_CODES) if (message.includes(c)) return true;
|
|
59
|
+
if (/fetch failed/i.test(message)) {
|
|
60
|
+
// "fetch failed" alone is Node's generic wrapper — it is only a
|
|
61
|
+
// transport casualty when its cause names a network fault, never on
|
|
62
|
+
// message text alone (a fetch can also fail on a non-network throw
|
|
63
|
+
// upstream, and that must not silently qualify).
|
|
64
|
+
const causeCode = codeOf(err.cause);
|
|
65
|
+
if (causeCode) return true;
|
|
66
|
+
const causeMsg = String(err.cause?.message ?? '');
|
|
67
|
+
if (/bad record mac/i.test(causeMsg) || [...TRANSPORT_CODES].some((c) => causeMsg.includes(c))) return true;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarize what a run DID with its tools, from its gate-audit records
|
|
3
|
+
* alone. Pure and read-only: no IO, no spine writes, no verdict.
|
|
4
|
+
*
|
|
5
|
+
* Counting rules:
|
|
6
|
+
* - `llm` rows are provider rounds, never tool calls — always excluded.
|
|
7
|
+
* - a tool call counts toward `totalCalls`/`byTool`/`uniquePaths`/repeats
|
|
8
|
+
* only when `decision === 'allow'`; a `deny` is tallied separately in
|
|
9
|
+
* `denied` and touches nothing else (denied ≠ done).
|
|
10
|
+
* - `byTool` is keyed by `action.args.tool` when present, else
|
|
11
|
+
* `action.type` (an edit or ctx_remember has no `args.tool`).
|
|
12
|
+
* - exact repeats: see {@link repeatKey}. A first occurrence of an action
|
|
13
|
+
* is not itself a "repeat" — only the 2nd, 3rd, ... occurrences count,
|
|
14
|
+
* so `repeats` can never exceed `totalCalls - byTool-group-count`.
|
|
15
|
+
* @param {Array<{run_id?: string, action?: {type?: string, path?: string, args?: any}, decision?: string|null}>} events
|
|
16
|
+
* @param {{runId?: string}} [opts]
|
|
17
|
+
* @returns {{totalCalls: number, byTool: Record<string, number>, denied: number,
|
|
18
|
+
* uniquePaths: number, repeats: number, repeatPct: number|null}}
|
|
19
|
+
*/
|
|
20
|
+
export function runBehaviour(events: Array<{
|
|
21
|
+
run_id?: string;
|
|
22
|
+
action?: {
|
|
23
|
+
type?: string;
|
|
24
|
+
path?: string;
|
|
25
|
+
args?: any;
|
|
26
|
+
};
|
|
27
|
+
decision?: string | null;
|
|
28
|
+
}>, { runId }?: {
|
|
29
|
+
runId?: string;
|
|
30
|
+
}): {
|
|
31
|
+
totalCalls: number;
|
|
32
|
+
byTool: Record<string, number>;
|
|
33
|
+
denied: number;
|
|
34
|
+
uniquePaths: number;
|
|
35
|
+
repeats: number;
|
|
36
|
+
repeatPct: number | null;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Render a {@link runBehaviour} summary as the agreed printable block
|
|
40
|
+
* (2–4 short lines). A zero-call run formats plainly ("0 tool calls") rather
|
|
41
|
+
* than a NaN-laced percentage — an empty run is a complete, reportable
|
|
42
|
+
* answer, not a division error.
|
|
43
|
+
* @param {ReturnType<typeof runBehaviour>} summary
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function formatBehaviour(summary: ReturnType<typeof runBehaviour>): string;
|
package/types/index.d.ts
CHANGED
|
@@ -22,4 +22,5 @@ export { runAuthorScout, buildSeedListing, seedFileList, classifySurvey, AUTHOR_
|
|
|
22
22
|
export { scrubRaw, RAW_PERSIST_MAX, RAW_TRIM_MARKER } from "./text.js";
|
|
23
23
|
export { authorClose, authorPrompt, declarationSchema, makeLoopGenerate, MAX_REVISIONS, QUESTION_SETS, GREEN_QUESTIONS, SOFTGREEN_QUESTIONS, CLASS_STATEMENTS, questionsFor, requiredAnswersFor } from "./authorflow.js";
|
|
24
24
|
export { validateCloseDecl, runDeclaredStages, declaredStages, isDeclaredClose, guardNames, closeGrade, DECLARED_GENRES, DECLARED_GAP_PREFIX, DECLARED_GAP_KEEP, DECLARED_CLOSE_CLASSES, CLOSE_DECL_FIELDS, HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED, HUMAN_CHECKPOINTS } from "./declaredclose.js";
|
|
25
|
+
export { runBehaviour, formatBehaviour } from "./behaviour.js";
|
|
25
26
|
export { runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalEvents, refuseLockedKind, GENRE, REFUSAL_LIB, REFUSAL_CATEGORY, AUTHORED_SPEC_FIELDS } from "./authorjob.js";
|
package/types/readshim.d.ts
CHANGED
|
@@ -36,15 +36,21 @@ export function readShimStrategy(arm: ReadShimArm): string;
|
|
|
36
36
|
* reference to the ledger — see its own note. Both live in one object because
|
|
37
37
|
* they are one worker's shim: the read seam and the retrieval seam the cap
|
|
38
38
|
* steers a capped worker towards.
|
|
39
|
-
* @param {{cap?: number, arm?: ReadShimArm
|
|
40
|
-
* shipped value is `READ_SHIM_CAP`; the option
|
|
41
|
-
* two bounds to one number rather than two
|
|
42
|
-
* levers are live (`readShimArm`). Defaults to
|
|
43
|
-
* before the arms existed keeps the behaviour it
|
|
39
|
+
* @param {{cap?: number, arm?: ReadShimArm, onCount?: ((kind: 'pointered'|'capped', bytesWithheld: number) => void)|null}} [opts]
|
|
40
|
+
* `cap`: delivery bound in bytes (the shipped value is `READ_SHIM_CAP`; the option
|
|
41
|
+
* exists so the native path can pin the two bounds to one number rather than two
|
|
42
|
+
* constants drifting apart). `arm`: which levers are live (`readShimArm`). Defaults to
|
|
43
|
+
* ALL of them, so every caller written before the arms existed keeps the behaviour it
|
|
44
|
+
* was written against. `onCount`: MEMORY-CACHE — fired once per pointer/capped-slice
|
|
45
|
+
* response with exactly the bytes withheld on THAT response, for a caller (e.g.
|
|
46
|
+
* `runPlan`) that sums several workers' shims into one run-level readout. Never called
|
|
47
|
+
* for a diff response (L2 is its own lever, uncounted here) or a first read under the
|
|
48
|
+
* cap (nothing was withheld).
|
|
44
49
|
*/
|
|
45
|
-
export function createReadShim({ cap, arm }?: {
|
|
50
|
+
export function createReadShim({ cap, arm, onCount }?: {
|
|
46
51
|
cap?: number;
|
|
47
52
|
arm?: ReadShimArm;
|
|
53
|
+
onCount?: ((kind: "pointered" | "capped", bytesWithheld: number) => void) | null;
|
|
48
54
|
}): {
|
|
49
55
|
arm: ReadShimArm;
|
|
50
56
|
cap: number;
|
|
@@ -56,6 +62,11 @@ export function createReadShim({ cap, arm }?: {
|
|
|
56
62
|
execute: (args: any) => Promise<any>;
|
|
57
63
|
};
|
|
58
64
|
serveStale: ({ file, path, startLine, endLine, detail }: StaleReq) => StaleServe | null;
|
|
65
|
+
summary: () => {
|
|
66
|
+
pointered: number;
|
|
67
|
+
capped: number;
|
|
68
|
+
bytesWithheld: number;
|
|
69
|
+
};
|
|
59
70
|
};
|
|
60
71
|
/**
|
|
61
72
|
* The one-tool spelling, kept for every caller written before the shim became an
|
package/types/reuse.d.ts
CHANGED
|
@@ -589,6 +589,28 @@ export const REUSE_GRADED_RED: readonly string[];
|
|
|
589
589
|
* how they would come to disagree about which member is which: the first three are
|
|
590
590
|
* allowances the loop's remaining tries are meant to spend against, and the human
|
|
591
591
|
* ones are the stop no further try can change (see `hardStop`).
|
|
592
|
+
*
|
|
593
|
+
* `provider-red` folded in 2026-08-25 (PRD v1.80 TODO #4, F115 "Ruled: one transport
|
|
594
|
+
* retry"; hamr's ruling verbatim: *"joins resume anyways and you decide but you are
|
|
595
|
+
* given an honest readout and with 1 retry"*). It is a MACHINE halt, not a human one
|
|
596
|
+
* — it does NOT go into `HUMAN_CHECKPOINTS` — but it is a checkpoint by the same
|
|
597
|
+
* test every other member here passes: the close never got the say it was asked
|
|
598
|
+
* for (no verdict was rendered, so it is not a graded red — see `REUSE_GRADED_RED`
|
|
599
|
+
* above), and the scout+draft and every finished step it bought stand on disk
|
|
600
|
+
* exactly like a cap-halt or a wall-halt. The one transport retry
|
|
601
|
+
* (`src/planrun.js`'s `withTransportRetry`) already covers the common case; when
|
|
602
|
+
* that retry ALSO fails, throwing the run away because the transport hiccuped
|
|
603
|
+
* twice is throwing away real work for no reason a machine can judge. Deliberately
|
|
604
|
+
* NO cost/step threshold gates this: a threshold is a number picked from a small
|
|
605
|
+
* observed sample (F115 logged exactly 7 rows), which is arbiter territory
|
|
606
|
+
* reserved for hamr — the operator decides case by case, every time, off the
|
|
607
|
+
* honest readout `scripts/run-u.mjs` prints.
|
|
608
|
+
*
|
|
609
|
+
* This entry was built 2026-08-24 as a LOCAL widening in `scripts/run-u.mjs`
|
|
610
|
+
* (`RESUMABLE_HALTS = [...CHECKPOINT_OUTCOMES, 'provider-red']`) because this file
|
|
611
|
+
* sat outside that pass's scope — the fold here is the follow-up, so this repo
|
|
612
|
+
* does not end up with two lists that can drift, which is the exact failure this
|
|
613
|
+
* constant exists to end (see the paragraph above).
|
|
592
614
|
*/
|
|
593
615
|
export const CHECKPOINT_OUTCOMES: readonly string[];
|
|
594
616
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is `err` a transport-class failure — the request never produced an HTTP
|
|
3
|
+
* response? True for: a TLS fault mid-record ("bad record mac" and kin), a
|
|
4
|
+
* reset/broken/timed-out socket (ECONNRESET, EPIPE, ETIMEDOUT, ECONNREFUSED,
|
|
5
|
+
* ENETUNREACH — by code on the error itself or on `err.cause`), and
|
|
6
|
+
* `fetch failed` wrapping a network `cause`. False for anything carrying an
|
|
7
|
+
* HTTP status (`err.status`/`err.statusCode`, 4xx/5xx/429) or an explicit
|
|
8
|
+
* `err.retryable === false` — those are provider RESPONSES, not transport
|
|
9
|
+
* casualties, and bare-agent's own policy governs them unchanged.
|
|
10
|
+
* @param {any} err
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function isTransportFailure(err: any): boolean;
|
|
14
|
+
/** The fixed retry budget: ONE extra attempt after the first transport
|
|
15
|
+
* throw. Never read from a job spec or CLI flag — raising it is arbiter
|
|
16
|
+
* territory (only a code change hamr signs, never a run-time knob). */
|
|
17
|
+
export const TRANSPORT_RETRIES: 1;
|
|
18
|
+
/** bare-agent's `Retry.call` counts ATTEMPTS, not retries. */
|
|
19
|
+
export const TRANSPORT_MAX_ATTEMPTS: number;
|