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
8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -1141,10 +1141,10 @@ separate browser consent. The explicit model off switch overrides both permissio

The same SQLite store holds only that answer, model-attempt timestamps and, for 30 days, the
random id of the job each attempt was charged to, never session ids or content. A transaction reserves
one of twelve reader-requested attempts in a rolling twenty-four hours before the model starts. Eligibility checks and a known missing CLI spend nothing; a failed
one of 100 reader-requested attempts in a rolling twenty-four hours before the model starts. Eligibility checks and a known missing CLI spend nothing; a failed
or timed-out attempt keeps its reservation because the provider may already have spent capacity.
Concurrent processes share the transaction bound. A denied or corrupt store fails closed. The page
reports the cap and the earliest time another attempt can be admitted. Turning permission off,
reports the cap, once it is spent, and the earliest time another attempt can be admitted. Turning permission off,
back on, or forgetting it does not erase unexpired spend timestamps or refill the budget.

The store is created owner-only; SQLite is required for this permission path. Deleting the file
Expand Down Expand Up @@ -1187,7 +1187,7 @@ An answer saved before the split reads as the Codex answer it was. "Turn off rea
`--forget` revoke every provider at once. So does an older build's Turn off, which knows only the
legacy Codex row: triggers in the store's own schema clear every other provider's answer and every
tool-output grant whenever that row is written as off, so a rollback and re-upgrade cannot bring a
Claude Code answer back. The twelve-attempt cap is shared between providers, so a
Claude Code answer back. The 100-attempt cap is shared between providers, so a
second provider cannot double it, and a spent budget is reported as `daily-cap` ahead of a missing
answer, whichever provider the status names.

Expand Down Expand Up @@ -3716,7 +3716,7 @@ event ingress: the harm is a side effect on the operator's Codex balance rather
about a session, and what holds it is the same-origin check and the refusal of a document
navigation, neither of which a local process sends headers for. A local process can grant the
remembered answer too, including the tool-output grant for the destination the board names, which
sends a Claude Code session's checks only to the reader's own configured vendor. The rolling cap of twelve attempts across tabs and processes bounds that
sends a Claude Code session's checks only to the reader's own configured vendor. The rolling cap of 100 attempts across tabs and processes bounds that
exposure. One reading per session may be in flight, and the explicit model off switch refuses all
calls. The
route reads nothing back to the caller beyond the job it started or found running, which the payload
Expand Down
18 changes: 10 additions & 8 deletions compass/skills/compass/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,8 @@ An older reading says coverage was not recorded. A tail starting inside the inte
every intent row "may be in the part not read"; claims stay independent. On Claude Code an Analyze
press reads the session's messages back to the window start where it can, and says where it stopped
when it cannot. General scope and the
count-by-kind cutoff remain under What it read. Coverage holds only counts, time and closed state
count-by-kind cutoff remain under What the analysis read, which groups the model, your intent, the
session read and what is never read under their own headings. Coverage holds only counts, time and closed state
tokens and changes no verdict. Each line reads "Departs; evidence #n", "Consistent with
#n, as the tool reported; not inspected", "Consistent with what the agent said at #n; not a
check" where it rests on the agent's messages alone, "Consistent with what the session said at #n;
Expand Down Expand Up @@ -543,14 +544,15 @@ A withdrawn claim draws no row. A claim alone offers Steer back as a secondary e
`You said "<its first sentence>" at 14:02 (entry #12 in Compass's activity list); the record shows otherwise at 14:05 (entry #13 in Compass's activity list).`
Short titles and bare URLs are not quoted in the correction. "Where the work went" groups the written files by folder. A
current result offers no second run control. When your intent changed after it or new work arrived
since, the result gives that reason with one "Analyze again". The request count and daily cap stay
beside it, and
"Not accurate?" marks the reading with a token that is stored beside it and never sent or counted;
that reading no longer contributes to the level or Steer back until you clear the mark. A later
since, the result gives that reason with one "Analyze again". The request count stays beside it.
The shared budget is not shown until it is spent; then a banner holds the top of the session page,
naming the limit and the time the next analysis can run. "Mark as inaccurate" marks the reading with
a token that is stored beside it and never sent or counted, and says so beside the control; that
reading no longer sets the level or Steer back until you press Undo, and the level's slot says why
it is empty. A later
direction alone offers no correction. A correction puts departures and failed checks first, dates
a time from an earlier day, and asks for evidence where an outcome is not shown. Its button names
the triggering entry and age; the daily reading budget remains beside the control, and What it
read counts new work since the analysis.
the triggering entry and age, and What the analysis read counts new work since the analysis.
The box offers Cancel, which stops the call and records a cancelled attempt that still counts, or one that spent nothing when it landed before anything was sent. A failed request says its result could
not be confirmed and is never retried automatically. The counter reports recorded model requests, so a refusal before the model runs
does not increase it.
Expand Down Expand Up @@ -745,7 +747,7 @@ Paths 2 and 3 are complementary and can both be installed. Keep `Notification` o
| `--frontend-dev-manifest PATH` | Contributor-only ticket for an owned Vite child, requiring a foreground server on `127.0.0.1`. Startup validates isolated fixture roots and a fresh child handshake; models, quota fetching and native actions are disabled. Diagnose/forget refuse this mode. Ordinary installed launches use the packaged page. |
| `--claude-reading-model MODEL` | Select an explicit Claude Sonnet or Opus generation 5 or later ID for reader-requested Claude readings (default `claude-sonnet-5-5`). `claude-sonnet-5` is the baseline. Unavailable models fail without retrying another selection; aliases and lower or unknown families refuse before launch. This selection supplies no accuracy qualification. |
| `--allow-tool-output` | Grant, at launch, sending a Claude Code reading's checks (the command, the result and the last 180 characters of output) and the paths it wrote, for the destination Compass names. The first press still asks for the words, and the disclosure says tool output was allowed when the dashboard started. `COMPASS_ALLOW_TOOL_OUTPUT=1` does the same, and `--no-observer-model` refuses it. The grant is kept like an Allow: Turn off readings and `--forget` clear it. |
| `--observer-model` / `--no-observer-model` | Offer optional Codex goal summaries, or refuse every model call for this run (refusal wins, including unasked checks). Goal summaries are off by default and retain their separate browser consent. On a goal-less Claude Code or Codex session the goal is drafted from the published first prompt (the latest where no first one with a time is published), and Analyze drift or Save intent adopts it. A session whose first prompt is a harness control such as `/clear` drafts nothing, says so, and never drafts a later prompt in its place. The goal field also offers the latest prompt without checking. Adopted goals say "from your prompt" and never enable unasked readings; editing the goal makes typed words. Reader-requested readings need no startup flag: the first Analyze drift presents its disclosure and Allow and analyze. The answer is remembered across tabs and restarts while the destination its disclosure named is unchanged, and asked for again once it moves; Turn off readings revokes it. They spend the capacity of the provider the disclosure names (Claude Code for a Claude Code session when `claude` is on PATH, Codex otherwise) and are capped at twelve reserved or actual attempts per rolling twenty-four hours. Failed attempts are not refunded; off/on and `--forget` do not reset that budget. Each prompt is bounded to 16,384 redacted bytes, with one call in flight per session; a reading has a 180-second timeout and a goal summary a 60-second one. Quota consent authorizes neither path. |
| `--observer-model` / `--no-observer-model` | Offer optional Codex goal summaries, or refuse every model call for this run (refusal wins, including unasked checks). Goal summaries are off by default and retain their separate browser consent. On a goal-less Claude Code or Codex session the goal is drafted from the published first prompt (the latest where no first one with a time is published), and Analyze drift or Save intent adopts it. A session whose first prompt is a harness control such as `/clear` drafts nothing, says so, and never drafts a later prompt in its place. The goal field also offers the latest prompt without checking. Adopted goals say "from your prompt" and never enable unasked readings; editing the goal makes typed words. Reader-requested readings need no startup flag: the first Analyze drift presents its disclosure and Allow and analyze. The answer is remembered across tabs and restarts while the destination its disclosure named is unchanged, and asked for again once it moves; Turn off readings revokes it. They spend the capacity of the provider the disclosure names (Claude Code for a Claude Code session when `claude` is on PATH, Codex otherwise) and are capped at 100 reserved or actual attempts per rolling twenty-four hours. Failed attempts are not refunded; off/on and `--forget` do not reset that budget. Each prompt is bounded to 16,384 redacted bytes, with one call in flight per session; a reading has a 180-second timeout and a goal summary a 60-second one. Quota consent authorizes neither path. |
| `--interaction-origin-session <harness:sid>` / `--interaction-origin-registration-file <private-file>` | Enable the prototype terminal for one exact session only when both flags are supplied. The generated private file supplies the registration capability; registration must happen inside that session's tmux pane. Output is read-only and bounded; no terminal input is accepted. |
| `--port N` | Change port (default 4553; valid range 1–65535). If the port is busy, check `--status` first — a running dashboard may already be there; don't kill it blindly. |
| `--host A` | Bind address: `127.0.0.1` (default) or `0.0.0.0`, IPv4 only. Nothing narrower — a single-interface bind is refused rather than half-supported, because `--status`, `--stop` and the hook forwarders all reach the dashboard over loopback and such a bind does not answer there. **Nothing authenticates a remote reader**: anything that reaches the port reads every session's titles, prompts and paths, and can answer a question a session is waiting on. Prefer `ssh -L 4553:127.0.0.1:4553`; use `--host` only on a network the user would hand the transcripts to. |
Expand Down
91 changes: 57 additions & 34 deletions compass/skills/compass/compass_runtime/reading.py
Original file line number Diff line number Diff line change
Expand Up @@ -2044,7 +2044,7 @@ def _not_read(earlier: int, untimed: int, after_stop: int) -> str:
f"{_entries(after_stop)} after the last observed stop "
f"{'was' if after_stop == 1 else 'were'} not read"
)
return "".join(f"; {clause}" for clause in clauses)
return "".join(f" {clause[0].upper()}{clause[1:]}." for clause in clauses)


def cutoff_text(
Expand Down Expand Up @@ -2080,25 +2080,18 @@ def cutoff_text(
unread_checks = int(measured.get("unread_checks") or 0)
left_out = _not_read(earlier, untimed, after_stop)
empty = (
f"No entry in the observed record was read{left_out}."
"No entry in the observed record was read."
if not total
else f"None of the {_entries(total)} after your words could be read{left_out}."
else f"None of the {_entries(total)} after your intent could be read."
)
mix = _cutoff_mix(selected)
stamped = [row["at"] for row in selected if row["at"] > 0]
if not stamped:
window = "none of them carrying a usable time"
else:
hours = max(0, int((now - min(stamped)) // 3600))
window = f"the oldest about {hours}h ago" if hours else "all within the last hour"
if len(stamped) != len(selected):
window += f", {len(selected) - len(stamped)} carrying no usable time"
window = _cutoff_window(selected, now)
partial = tail_truncated is True and tail_start is not None and tail_start > window_start > 0
tail_note = (
"The message tail starts "
f"{time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime(tail_start))}; "
" The message tail starts "
f"{time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime(tail_start))}, so "
f"about {max(1, math.ceil((tail_start - window_start) / 60))} min at the start "
"of the window were outside it."
"of the window were not read."
if partial
else ""
)
Expand All @@ -2110,13 +2103,12 @@ def cutoff_text(
f" {early} agent message{'s' if early != 1 else ''} that closed an earlier turn "
f"{'were' if early != 1 else 'was'} kept."
)
read = _cutoff_read(len(selected), total, partial=partial)
opening = (
f"Read {len(selected)} entries. " + tail_note
if partial
else f"Read {len(selected)} of the {_entries(total)} available after your words."
f"{read}: {mix}. {window}{tail_note}{left_out}"
if selected
else empty + tail_note + left_out
)
if not selected:
opening = empty + (" " + tail_note if partial else "")
omissions = []
if tail_truncated is True and measured.get("tail_start") is None:
# The page says this too. A read that stopped before the window with nothing in it to name
Expand All @@ -2131,11 +2123,40 @@ def cutoff_text(
omissions.append(
f"{unread_checks} check{'s' if unread_checks != 1 else ''} had no room in the prompt"
)
return (
opening.rstrip(".") + left_out + f". Of those read, {window}: {mix}."
if selected
else opening
) + (" " + "; ".join(omissions) + "." if omissions else "")
if not omissions:
return opening
tail = "; ".join(omissions)
return f"{opening} {tail[0].upper()}{tail[1:]}."


def _cutoff_window(selected: Sequence[LedgerEntry], now: float) -> str:
"""How old the entries read are, as a sentence."""
stamped = [row["at"] for row in selected if row["at"] > 0]
one = len(selected) == 1
if not stamped:
return "It has no usable time." if one else "None of them has a usable time."
hours = max(0, int((now - min(stamped)) // 3600))
window = (
("It is" if one else "The oldest is") + f" from about {hours}h ago"
if hours
else ("It is" if one else "All are") + " from within the last hour"
)
if len(stamped) != len(selected):
window += f"; {len(selected) - len(stamped)} have no usable time"
return window + "."


def _cutoff_read(read: int, total: int, *, partial: bool) -> str:
"""The cutoff's opening clause: how many of the entries after the intent were read."""
if partial:
return f"Read {_entries(read)}"
if read == total:
return (
f"Read all {total} entries after your intent"
if total > 1
else ("Read the 1 entry after your intent")
)
return f"Read {read} of the {_entries(total)} after your intent"


def _cutoff_mix(selected: Sequence[LedgerEntry]) -> str:
Expand All @@ -2145,27 +2166,29 @@ def _cutoff_mix(selected: Sequence[LedgerEntry]) -> str:
subject, result = row.get("subject"), row.get("result")
name = (
(
"passed"
"passed check"
if result == "passed"
else "failed"
else "failed check"
if result == "failed"
else "run with no recorded result"
else "check with no recorded result"
)
if subject == CHECK_SUBJECT
else ("file written" if subject == WRITE_SUBJECT else "launch")
else ("file write" if subject == WRITE_SUBJECT else "launch")
)
elif row["author"] == AUTHOR_PERSON:
name = "you wrote"
name = "from you"
elif row["author"] == AUTHOR_DERIVED:
name = "Compass derived"
name = "derived by Compass"
else:
name = "message the agent wrote" if row["type"] == AGENT_MESSAGE_TYPE else "agent event"
name = "message from the agent" if row["type"] == AGENT_MESSAGE_TYPE else "agent event"
counts[name] = counts.get(name, 0) + 1
plurals = {
"message the agent wrote": "messages the agent wrote",
"message from the agent": "messages from the agent",
"agent event": "agent events",
"run with no recorded result": "runs with no recorded result",
"file written": "files written",
"passed check": "passed checks",
"failed check": "failed checks",
"check with no recorded result": "checks with no recorded result",
"file write": "file writes",
"launch": "launches",
}
return ", ".join(
Expand Down
2 changes: 1 addition & 1 deletion compass/skills/compass/compass_runtime/reading_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from .config import RuntimeConfig

DAY_SEC = 86_400.0
DAILY_CAP = 12
DAILY_CAP = 100
# How long the job ledger keeps the id each charge was made for (DRC-4713).
# Far past `DAY_SEC`, because the budget reads only `spends`: a restart more
# than a day after a crash must still find the row, or it would call a charged
Expand Down
4 changes: 2 additions & 2 deletions compass/skills/compass/compass_runtime/web/react.html

Large diffs are not rendered by default.

Loading
Loading