Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cargento/skills/cargento/cargento_runtime/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,7 @@ def _attach_command_reports(self, out_sessions: list[Session]) -> dict[str, Any]
if (report["harness"], report["sid"]) == (session["harness"], session["sid"])
]
return {
"ends_observable": self.overlays is not None,
"irreversible_enabled": enabled,
**({"command_reports": reports} if enabled else {}),
}
Expand Down
20 changes: 18 additions & 2 deletions cargento/skills/cargento/cargento_runtime/dismissals.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ def store_path(config: RuntimeConfig) -> str:
return os.path.join(config.state_home, "cargento-dismissals.json")


def is_matchable(harness: str, sid: str) -> bool:
"""Whether (harness, sid) can ever match a published session row.

Claude Code sessions publish the 8-character transcript prefix as their
sid, so a 36-character UUID or other non-8-character identifier can never
match any published Claude row. Admitting an unmatchable key would write a
permanent junk entry that never expires on activity, consuming one of the
bounded dismissal slots and eventually evicting a real dismissal (DRC-4179).
"""
if not harness or not sid:
return False
return not (harness == "claude" and len(sid) > 8)


def _entry(value: Any) -> Dismissal | None:
"""One untrusted record as a dismissal, or nothing.

Expand All @@ -79,7 +93,7 @@ def _entry(value: Any) -> Dismissal | None:
return None
harness = records.safe_text(value.get("harness"), KEY_CAP_CHARS).strip()
sid = records.safe_text(value.get("sid"), KEY_CAP_CHARS).strip()
if not harness or not sid:
if not is_matchable(harness, sid):
return None
return {
"harness": harness,
Expand Down Expand Up @@ -258,7 +272,7 @@ def dismiss(
if not config.dismissals_enabled:
return False
key = _key(harness, sid)
if not key[0] or not key[1]:
if not is_matchable(key[0], key[1]):
return False
stamp = time.time() if now is None else now
entry: Dismissal = {
Expand Down Expand Up @@ -290,6 +304,8 @@ def restore(
if not config.dismissals_enabled:
return False
key = _key(harness, sid)
if not is_matchable(key[0], key[1]):
return False
with state.dismissal_lock:
bounded = tuple(e for e in load(config) if (e["harness"], e["sid"]) != key)
state.dismissals = _stored(bounded)
Expand Down
1 change: 0 additions & 1 deletion cargento/skills/cargento/cargento_runtime/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1201,7 +1201,6 @@ def _dismiss(self) -> None:
answer = {
"ok": True,
"persisted": persisted,
"cleared": len(dismissals.active(config, state)),
}
self._send(json.dumps(answer, separators=(",", ":")).encode(), "application/json")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ function nextObserved(payload, evidence){
quiet: `The other ${other.length}: ${otherWords.filter(word => word[1]).map(word => `${word[1]} ${word[0]}`).join(" · ") || "none"}; ` +
`of these, ${partial} partially read.`,
gates: `${totals.reportsBlock} of ${totals.sessions} ${totals.sessions === 1 ? "session reports" : "sessions report"} block state · ` +
`${totals.sessions - totals.reportsBlock} unknown · ends observed on ${totals.ended} ${totals.ended === 1 ? "session" : "sessions"}`,
`${totals.sessions - totals.reportsBlock} unknown · ${payload.ends_observable === false ? "ends unobservable" : `ends observed on ${totals.ended} ${totals.ended === 1 ? "session" : "sessions"}`}`,
rows: harnesses.map(row => ({key: String(row.key || ""), label: nextObservedString(row.label) || String(row.key || "Harness not published"),
sessions: sessions.filter(session => session.harness === row.key).length,
...nextObservedPair("block", !row.error && row.reports_needs_input === true ?
Expand Down
36 changes: 19 additions & 17 deletions cargento/skills/cargento/cargento_runtime/web/next-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,24 +112,26 @@ function nextSessionMeta(session){
const parts = [];
const harness = nextSessionRegistryLabel(session);
if(harness) parts.push(harness);
if(session.state_detail) parts.push(String(session.state_detail));
/* Outside the state chain below, on purpose. An end is a fact about the
session id; `state` is a reading of file recency that a session which ended
seconds ago can still make say "working". */
/* An observed end supersedes the present-tense activity and duration phrases;
an ended session must not describe itself as awaiting input (DRC-4554). */
const ended = nextDurationSince(nextSessionEndedAt(session));
if(ended != null) parts.push(`ended ${ended} ago`);
if(session.state === "needs_input"){
const blocked = nextDurationSince(session.blocked_since);
if(blocked != null) parts.push(`blocked ${blocked}`);
if(session.wait_unconfirmed) parts.push("unconfirmed: no positive observation in 5m");
}else if(session.state === "working"){
const turn = session.turn;
const elapsed = turn && typeof turn === "object" && !Array.isArray(turn) &&
typeof turn.elapsed_h === "string" ? turn.elapsed_h.trim() : "";
if(elapsed) parts.push(`turn started ${elapsed} ago`);
}else if(session.state === "idle"){
const started = nextDurationSince(session.started_at);
if(started != null) parts.push(`session started ${started} ago`);
if(ended != null){
parts.push(`ended ${ended} ago`);
}else{
if(session.state_detail) parts.push(String(session.state_detail));
if(session.state === "needs_input"){
const blocked = nextDurationSince(session.blocked_since);
if(blocked != null) parts.push(`blocked ${blocked}`);
if(session.wait_unconfirmed) parts.push("unconfirmed: no positive observation in 5m");
}else if(session.state === "working"){
const turn = session.turn;
const elapsed = turn && typeof turn === "object" && !Array.isArray(turn) &&
typeof turn.elapsed_h === "string" ? turn.elapsed_h.trim() : "";
if(elapsed) parts.push(`turn started ${elapsed} ago`);
}else if(session.state === "idle"){
const started = nextDurationSince(session.started_at);
if(started != null) parts.push(`session started ${started} ago`);
}
}
/* These last two are unconditional, and both for the reason the first one
gives: every clause above is a reading, and these say what the readings
Expand Down
34 changes: 34 additions & 0 deletions cargento/skills/cargento/tests/test_dismissals.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,5 +362,39 @@ def test_the_defaults_are_bounded(self) -> None:
self.assertIs(True, dataclasses.replace(config, dismissals_enabled=True).dismissals_enabled)


class MatchableKeyTest(DismissalStoreTestCase):
"""DRC-4179: an unmatchable key cannot consume a slot or evict a real dismissal."""

def test_an_unmatchable_claude_uuid_is_refused(self) -> None:
config, state = self.runtime()
# Full 36-char UUID for Claude: can never match an 8-character prefix row.
uuid_sid = "12345678-1234-1234-1234-123456789abc"
persisted = dismissals.dismiss(config, state, "claude", uuid_sid, now=1000.0)
self.assertFalse(persisted)
self.assertEqual((), dismissals.load(config))

def test_an_unmatchable_entry_in_store_is_dropped_on_load(self) -> None:
config, _state = self.runtime()
self.write_store(
{
"v": 1,
"entries": [
# Real 8-char Claude sid
{"harness": "claude", "sid": "abcd1234", "at": 100.0, "seen_activity": 100.0},
# Junk 36-char UUID sid that can never match
{
"harness": "claude",
"sid": "12345678-1234-1234-1234-123456789abc",
"at": 200.0,
"seen_activity": 200.0,
},
],
}
)
loaded = dismissals.load(config)
self.assertEqual(1, len(loaded))
self.assertEqual("abcd1234", loaded[0]["sid"])


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion cargento/skills/cargento/tests/test_focus.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,7 @@ def test_the_pinned_assembly_is_untouched(self) -> None:
# would fail on the reader rather than on an injected token.
self.assertNotIn(b'<meta name="cargento-focus"', assembled)
self.assertEqual(
"f7cd56b1aa750b7b8980d4e20e1e55792fba13d995de22497bd732eb5c9afa63",
"7c0dbcb341cd1652e60821fde01b3c1080bc58022100bfa9ba25317fefb1c52a",
hashlib.sha256(assembled).hexdigest(),
)

Expand Down
24 changes: 22 additions & 2 deletions cargento/skills/cargento/tests/test_http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ def test_a_mark_reaches_the_store_and_the_next_read_reflects_it(self) -> None:
self.assertEqual((200, 200, 200), (first, status, listed_status))
answer = json.loads(body)
self.assertIs(True, answer["persisted"])
self.assertEqual(1, answer["cleared"])
self.assertNotIn("cleared", answer)
self.assertEqual(
[{"harness": "claude", "sid": "abcd1234"}],
[{k: v for k, v in row.items() if k != "at"} for row in json.loads(listed)["cleared"]],
Expand All @@ -656,7 +656,27 @@ def test_a_body_that_names_nothing_is_a_no_op(self) -> None:
with self.subTest(body=body):
status, answer = self._post(port, body)
self.assertEqual(200, status)
self.assertEqual(0, json.loads(answer)["cleared"])
self.assertNotIn("cleared", json.loads(answer))

def test_an_unmatchable_key_returns_persisted_false_and_does_not_persist(self) -> None:
config, state = self._runtime()
with self._serving(cli.build_application(config, state, clock=time.time)) as port:
status, body = self._post(
port,
json.dumps(
{
"harness": "claude",
"sid": "12345678-1234-1234-1234-123456789abc",
}
).encode(),
)
listed_status, listed = self._get(port, "/api/cleared")
self.assertEqual(200, status)
self.assertEqual(200, listed_status)
answer = json.loads(body)
self.assertIs(False, answer["persisted"])
self.assertNotIn("cleared", answer)
self.assertEqual([], json.loads(listed)["cleared"])

def test_the_rollback_switch_answers_503_on_both_routes(self) -> None:
# 503, not 404: under `--no-dismiss` the route exists and the store does
Expand Down
12 changes: 10 additions & 2 deletions cargento/skills/cargento/tests/test_next_attention.py
Original file line number Diff line number Diff line change
Expand Up @@ -2261,8 +2261,8 @@ def model(self, sessions: list[dict[str, Any]]) -> Any:
)
)

def render(self, sessions: list[dict[str, Any]]) -> str:
payload = {"generated": 10_000, "sessions": sessions}
def render(self, sessions: list[dict[str, Any]], **extra: Any) -> str:
payload = {"generated": 10_000, "sessions": sessions, **extra}
rendered = self._run_page_js(
"\n".join(
(
Expand Down Expand Up @@ -2410,6 +2410,14 @@ def test_the_visible_coverage_line_counts_no_end_as_none_not_undefined(self) ->
self.assertIn("ends observed on 0 sessions", visible)
self.assertNotIn("undefined", visible)

def test_the_visible_coverage_line_says_unobservable_under_no_events(self) -> None:
# DRC-4555: under --no-events ends are unobservable, not zero observed.
html = self.render([self.row(sid="quiet-1")], ends_observable=False)
visible = html.split('<details class="next-attention-coverage-details"')[0]

self.assertIn("ends unobservable", visible)
self.assertNotIn("ends observed on 0 sessions", visible)


@unittest.skipUnless(shutil.which("node"), "node not available")
class HealthyBoardBriefTest(NextPageJsHarness):
Expand Down
4 changes: 2 additions & 2 deletions cargento/skills/cargento/tests/test_next_flag.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ def test_retired_next_query_is_not_a_page_alias(self) -> None:
def test_the_canonical_loader_is_the_released_ui_bundle(self) -> None:
page = frontend_page.load_page()

self.assertEqual(911_238, len(page))
self.assertEqual(911_302, len(page))
self.assertEqual(
"f7cd56b1aa750b7b8980d4e20e1e55792fba13d995de22497bd732eb5c9afa63",
"7c0dbcb341cd1652e60821fde01b3c1080bc58022100bfa9ba25317fefb1c52a",
hashlib.sha256(page).hexdigest(),
)

Expand Down
14 changes: 14 additions & 0 deletions cargento/skills/cargento/tests/test_next_observed.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ def test_every_denominator_comes_from_the_same_thirteen_sessions(self) -> None:
self.assertEqual(["alpha", "epsilon", "beta"], out["activeProjects"])
self.assertEqual(["zeta", "delta", "gamma", "theta"], out["rest"])

def test_gates_coverage_distinguishes_unobservable_ends_from_none_observed(self) -> None:
# DRC-4555: under --no-events ends are unobservable, not zero observed.
out = self._run_page_js(
self.FIXTURE
+ """
payload.ends_observable = false;
console.log(JSON.stringify(nextObserved(payload).coverage.gates));
"""
)
self.assertEqual(
"9 of 13 sessions report block state · 4 unknown · ends unobservable",
out,
)

def test_every_text_has_a_nonempty_value_and_known_boolean(self) -> None:
out = self._run_page_js(
self.FIXTURE
Expand Down
12 changes: 6 additions & 6 deletions cargento/skills/cargento/tests/test_next_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,8 +616,8 @@ def test_load_page_preserves_its_byte_oracles(self) -> None:
"b71d627fe8cc06bc4210d23c76c0ee5b2ab644ce15a44c9d617ba66fe398847d",
),
"next-observed.js": (
31_136,
"931161c99c97d8f72fd30e7c9b104546541979b82eb7c8dec17bb4682d32edf2",
31_199,
"05d0574cd61663223ee75e8bb9eed8d10729733b737d79a4fa8943793116cb89",
),
"next-attention.js": (
56_558,
Expand Down Expand Up @@ -664,8 +664,8 @@ def test_load_page_preserves_its_byte_oracles(self) -> None:
"62f971c5e2a570068b7e2c3ee72b2499774d14a3b739f6f908962f91b98382f1",
),
"next-session.js": (
36_503,
"08099b828eb65c1c6c07f214a6e90c1cf18547249f5174e3aaf4379fb77d7bd1",
36_504,
"4a338ab2c9ce6ae7c197e1361df807a54bbf17b05078541cdba26eeef9fc1c9c",
),
"next-workstream.js": (
18_659,
Expand Down Expand Up @@ -707,9 +707,9 @@ def test_load_page_preserves_its_byte_oracles(self) -> None:
)

assembled = frontend_page.load_page()
self.assertEqual(911_238, len(assembled))
self.assertEqual(911_302, len(assembled))
self.assertEqual(
"f7cd56b1aa750b7b8980d4e20e1e55792fba13d995de22497bd732eb5c9afa63",
"7c0dbcb341cd1652e60821fde01b3c1080bc58022100bfa9ba25317fefb1c52a",
hashlib.sha256(assembled).hexdigest(),
)

Expand Down
15 changes: 15 additions & 0 deletions cargento/skills/cargento/tests/test_next_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1343,3 +1343,18 @@ def test_a_session_with_no_observed_end_still_reads_idle(self) -> None:

self.assertNotIn("session ended", html)
self.assertIn("idle", html)

def test_an_ended_session_does_not_describe_itself_as_awaiting_your_message(self) -> None:
# DRC-4554: header yields present-tense state phrase to an observed end.
html = self.detail(
'state_detail: "awaiting your message", finished_at: 9350, ended_at: 9400,'
)

self.assertIn("ended 10m ago", html)
self.assertNotIn("awaiting your message", html)

def test_a_non_ended_session_describes_itself_as_awaiting_your_message(self) -> None:
html = self.detail('state_detail: "awaiting your message", finished_at: 9350,')

self.assertIn("awaiting your message", html)
self.assertNotIn("ended 10m ago", html)