Skip to content

Commit 7abcd53

Browse files
Record the fixed defects in README, and pin the issue format
The status section now states each defect that was closed and what it cost while open, in the same register as the rest of the page: the poisoned deadline guard, the async double-charge, the prose-bracket parser, the fan-out attribution, and the round event that was a measurement when it should have been an envelope. .github/ISSUE_TEMPLATE/task.md fixes the seven-section shape used by the ten issues opened against this repo, so every future issue arrives with a summary, why it matters, where in the code with a command to confirm it, what to change, how to verify, acceptance criteria, and an explicit skill level. It also states the house rule out loud: a change arrives with a test that fails without it, checked by reverting the source and watching the test go red. uv.lock is a stale-lock refresh, not a dependency change: pyproject already said 0.1.1 while the lock still said 0.1.0a0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 58cb5db commit 7abcd53

3 files changed

Lines changed: 123 additions & 3 deletions

File tree

.github/ISSUE_TEMPLATE/task.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
name: Task or defect
3+
about: The standard format for every issue in this repository
4+
title: "area: what is wrong or missing, stated plainly"
5+
labels: ''
6+
assignees: ''
7+
---
8+
9+
<!--
10+
Keep these seven sections, in this order, for every issue. The title prefix is
11+
the subsystem: runtime, planner, harness, observability, policy, session,
12+
server, cli, docs, packaging.
13+
14+
The rule this repo runs on: a claim needs evidence. Quote the code or paste the
15+
command output that shows the problem, rather than describing it from memory.
16+
-->
17+
18+
## Summary
19+
20+
What is wrong or missing, in one short paragraph. Quote the offending code or the
21+
real command output rather than paraphrasing it.
22+
23+
## Why this matters
24+
25+
What breaks, or what a user cannot do, as a consequence. Prefer a concrete
26+
failure over an adjective.
27+
28+
## Where in the code
29+
30+
- `path/to/file.py:LINE` — what is there and why it is relevant
31+
32+
Include a command a reader can run to confirm the problem for themselves:
33+
34+
```bash
35+
```
36+
37+
## What to change
38+
39+
1.
40+
2.
41+
42+
State anything deliberately out of scope, so a pull request does not grow past
43+
what was agreed here.
44+
45+
## How to verify
46+
47+
```bash
48+
uv run pytest -q
49+
uv run ruff check .
50+
```
51+
52+
Note which new test proves the fix. This repo's convention is that a change
53+
arrives with a test that **fails without it** — check that by reverting your
54+
source edit and watching the new test go red.
55+
56+
## Acceptance criteria
57+
58+
- [ ]
59+
- [ ] `uv run pytest` stays green and `uv run ruff check .` is clean
60+
- [ ] Any README or cookbook sentence this changes is updated in the same pull
61+
request (several are byte-compared against real output by the test suite)
62+
63+
## Skill level
64+
65+
Pick one and delete the other.
66+
67+
**good first issue** — say why it is well bounded, point at a sibling file that
68+
shows the pattern to copy, and invite questions on the issue.
69+
70+
**experience required** — say which subsystems the change spans, what could
71+
silently break, and ask for a design comment on the issue before any code is
72+
written.

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,15 +480,21 @@ Re-derived on 2026-07-28 by running each item, not by reading the commit log.
480480

481481
- **The HTTP API does not use the durable session layer.** It has its own `InProcessRuntime`, whose sessions die with the process and whose approvals are recorded without being delivered. [ROADMAP.md](ROADMAP.md) §12.3.
482482
- *Closed:* `grapharc plan` drives the governed loop; `PolicyEngine.edge_policy()` compiles the TOML document into the gate `AdmissionChecker` consults, and `grapharc plan --policy` is the caller; `grapharc demo --memory PATH` hands the shipped graphs the durable SQLite store.
483+
- *Closed:* the shipped registry withheld the trace recorder from its `PlannerNode` and `Materializer`, so `grapharc plan` wrote a file with no `plan` event and **no `start`/`end` pair for any node it executed** — the paragraph above claiming otherwise was true of a hand-wired loop and false of the one the command drives. Both now get the recorder, and a test asserts the phase counts.
483484

484485
**Real limits of things that do work**
485486

486487
- **Admission authorises a kind, not its arguments.** A proposal carrying `args={"path": "/etc/passwd"}` is admitted on the strength of its kind alone.
487488
- **The audit-hook sandbox is in-process confinement, not a kernel boundary.** `os.stat` outside the workspace is not blocked, because CPython raises no event for it. `ContainerExecutor` is the boundary where one is needed.
488489
- **`run_command` is not confined.** Argv-only and never a shell, but the child is an ordinary process with your privileges.
490+
- *Closed:* a `max_seconds` past the platform's `time_t``float("inf")`, or a plausible "effectively unlimited" like `1e10` — used to **disable the deadline guard for the rest of the process**. `setitimer` raised *after* the SIGALRM handler was installed and the process-wide slot taken, leaking both, so every later run silently fell back to the mechanism that cannot unwind a blocking syscall: a 0.3s ceiling then took a 5s sleep to notice. Arming is undone on failure now, and the armed delay is clamped to what both mechanisms accept.
491+
- *Closed:* every `async def` node **double-charged** its token re-reports. The re-report ledger was keyed by thread ident, but `on_llm_end` is sync — under `ainvoke` LangChain dispatches it to a worker thread while the body stays on the event loop — so the automatic charge found no ledger and the node's named re-report was charged again. Any node using the shipped `charge_usage`, `AgentNode._charge_tokens` or `planner.proposal._charge` reported double its real spend and hit `max_tokens` at half its declared allowance. The ledger is a `contextvars` scope now, which also fixes an inner scope discarding the enclosing node's.
492+
- *Closed:* a bracket anywhere in a model's **prose** hijacked JSON extraction, because only the first `{`/`[` was ever tried. `Based on the context [lines 3-5]: {…}` was rejected as unparseable, and — worse — `Analysis (note [1]): {"supported": false}` returned a perfectly valid `[1]`, substituting a fabricated value for the verifier's actual answer. Every opener is tried now, longest parse wins; junk still returns `None`, so fail-closed is unchanged.
489493
- **`interrupt()` suspends but cannot be resumed.** LangGraph's native interrupt stops the graph and shows on `get_state`, and there is no supported resume path — resuming means passing a `Command` as *input*, which is closed by design. Use the session layer's approval gate for human-in-the-loop.
490494
- **Still unwrapped from LangGraph:** `retry_policy`, `cache_policy`, `durability`, subgraphs. `.inner` reaches them, but execution entry points there fail closed, so `.inner` is an inspection escape hatch and not a way to run the graph.
491495
- **Cost is recorded when a backend reports one, estimated when it does not.** Both gateways publish the provider's `cost_usd` through the same `llm_output` envelope, the runtime's usage callback writes it onto the node's `end` event, and an agent's `model` events carry the per-call breakdown. A backend that reports no price still falls back to a `RateCard` estimate, and the two figures stay apart — `recorded_cost_usd` is never a guess. Still missing: no tenant on a trace event, so per-tenant attribution is not offered.
496+
- **A node's tokens are its own, not the run's movement while it ran.** Worth stating because it was the other way round: an `end` event carried the difference between two readings of the run's *shared* meter, so under fan-out the workers' windows overlapped and each was credited with its siblings' concurrent spend. Three workers costing 8 tokens each traced as 24/16/8, and `metrics` and `cost` agreed on 48 for 24 tokens of real work — doubling the estimated bill purely because the work ran in parallel. Attribution now comes from a per-node scope on the meter, so the same work costs the same serially and in parallel; a hand charge the usage callback never saw still lands on the node that made it.
497+
- **A planning round is an envelope, not a measurement.** A `round` event used to carry the planner's `tokens` and the round's `duration_ms`, both of which `metrics`, `cost` and `replay` add on top of node totals — and the planner's spend was already reported by its own `plan` event, so it was counted twice, and a round's duration encloses the plan plus every node it ran. Neither is on the event now; both are on its `state_delta` as `round_tokens` / `round_iterations` / `round_duration_ms`, where no reader sums them. `RoundRecord.iterations` also holds a figure now rather than always `0`.
492498
- **The Claude CLI backend is completion-only.** Tool calling and structured output need one of the OpenAI-wire backends: `openrouter`, `openai`, or a local `ollama`.
493499
- **A session turn is synchronous**, and a runner claim is a claim rather than a lease — nothing reclaims a session whose runner died holding it.
494500
- **A bare model spec resolves to the paid `claude-cli` backend.** `--model mock` does not reach the scripted double; it becomes the model name `mock` on the subscription backend. Only the slash form (`mock/anything`) reaches the double. A mistyped backend *with* a slash is rejected properly, exit 2.

uv.lock

Lines changed: 45 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)