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
15 changes: 7 additions & 8 deletions docs/agent/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ Deleting data remains a person's job.

`internal/attn` holds only the deterministic part: fact input, event classification, derivation,
atomic publication, expiry and reads. It does not open a network connection, read a key or call a model.
`internal/attn/model` holds the optional enrichment: the the model client, local settings, telemetry, preflight and
the runner.
`internal/attn/model` holds the optional enrichment: the model client, local settings, telemetry,
preflight and the runner.

`internal/cmd/attention_boundary_test.go` checks that boundary instead of leaving it as a comment:

Expand All @@ -66,12 +66,11 @@ the runner.
So with no key, no `attn.json` and an unreachable provider, the control path still works: nothing
leaves the machine, and no service start, stop or readiness decision depends on the enrichment.

The artifact itself is gated on consent. `status` publishes it only once the person has enabled the model,
which makes `attn_path` a signal an agent can act on directly: present means the enhancement is
on; absent means either nothing needs assessing or the user never opted in, and the agent no longer
has to tell those apart by reading the settings file. An install that turns the enhancement off also
clears what was published before, so a stale artifact cannot keep inviting an assessment the person
declined.
The artifact is not gated on consent: it is a local, code-derived fact, and publishing it sends
nothing anywhere. `attn_path` present means there is an anomaly; the adjacent `attn` block (consent,
fixed reason, assessment state) is what an agent reads to decide whether it may run `oberth attn
assess`. Turning the assessment off therefore keeps the fact-only handoff and changes only that
block — the person's refusal is never inferred from whether a local file exists.

## Read-only addresses and shared environment

Expand Down
22 changes: 13 additions & 9 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,15 +259,18 @@ file exists. Use logs for detailed output. `worktree.services[]`'s `pid`, `run_i
are provided while the service is being run by option-berth, so even a portless worker can be
verified against the actual run record through these fields; `log_path` is always given by the engine.

Once the optional attention enhancement is enabled, a definite abnormal event in the state also puts
`attn_path` in the JSON. It points at a recomputable, expirable agent handoff file under
`BERTH_HOME/attention/`; with no current abnormality the field is absent. **Its presence is the
signal that the person enabled the enhancement** — an agent that sees one may run `oberth attn
assess`, and an agent that sees none does not have to guess whether that is because nothing is wrong
or because the model was never turned on. Without consent `status` publishes nothing and clears an artifact
left over from before the enhancement was switched off. When there is an abnormality,
`attn_revision` is provided as well and must equal the file's `state_revision`, so the agent can
check inside the same `status` response whether the file still corresponds to current facts. An
A definite abnormal event in the state puts a fact-only handoff in the JSON, whether or not the
optional assessment is enabled. `attn_path` points at a recomputable, expirable agent handoff file
under `BERTH_HOME/attention/`; with no current abnormality the field is absent. **Its presence means
"there is an anomaly", not "the person opted in"** — publishing a local fact sends nothing anywhere,
so consent cannot decide whether it exists. The adjacent `attn` block says whether that handoff can
be assessed now: `enabled` is the consent bit, `reason` is the fixed code when nothing can run, and
`assessment` is `assessed` (the file already carries a validated result), `pending` (an enabled,
complete configuration could assess it), or `unavailable`. An agent may use the fact-only handoff
without a configured model; it must check `attn.enabled` before running `oberth attn assess`.
When there is an abnormality, `attn_revision` is provided as well and must equal the file's
`state_revision`, so the agent can check inside the same `status` response whether the file still
corresponds to current facts. An
undeclared listener is not an abnormality: `status` already lists the port with its owner, and a
process the agent did not start is not one it should stop. The core file is
`oberth.attn/v1`, containing `event_id`, `state_revision`, `worktree_root`, optional `branch`,
Expand Down Expand Up @@ -369,6 +372,7 @@ or renaming one.
| `observation.source` | `status` | The collector that answered when it was not the platform's primary one; non-empty means a missing port is unknown, not free. |
| `machine[].listening_known` | `status` | Whether `listening` was observed; false means the table cannot see that port's owner. |
| `attn_path` | `status` | The derived handoff file for the current abnormality; absent when there is none. |
| `attn` | `status` | Whether that handoff can be assessed now: `enabled`, fixed `reason`, `assessment` (`assessed`, `pending` or `unavailable`). |
| `services[].starting` | `status` | Instantaneous facts of an in-progress start; not a run and not the same as `running`. |
| `starting.phase` | `status` | `waiting_dependency`, `prepare` or `spawning`. |
| `starting.waiting_on` | `status` | Dependencies still being waited on; keeps the last known set when observation fails. |
Expand Down
12 changes: 8 additions & 4 deletions docs/cli.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,14 @@ changed 是第一次运行时缺省的可选字段。ports 仍是给脚本核对
`worktree.services[]` 的 `pid`、`run_id` 和 `started_at` 在服务由 option-berth 正在运行时提供,
无端口 worker 也能靠这些字段核对实际运行记录;`log_path` 始终由引擎给出。

在用户启用可选的模型评估之后,状态里出现确定的异常事件才会带 `attn_path`。它指向
状态里出现确定的异常事件就会带事实型交接文件,与是否启用可选评估无关。`attn_path` 指向
`BERTH_HOME/attention/` 下的一个可重算、可过期的 agent 交接文件;没有当前异常时该字段缺省。
**这个字段出现本身就是「用户已启用增强」的信号**:看到它的 agent 可以运行 `oberth attn assess`,
看不到的 agent 也不必再猜是「没有异常」还是「从没开过该评估」。未启用时 `status` 不发布,并清掉
关闭之前留下的文件。有异常时同时提供 `attn_revision`,它必须与文件中的 `state_revision`
**它出现表示「有异常」,不表示「用户已同意」**——发布一个本地事实不向任何地方发送数据,
所以同意不能决定它是否存在。相邻的 `attn` 块说明这份交接现在能否被评估:`enabled` 是同意位,
`reason` 是当前无法运行时的固定原因码,`assessment` 取 `assessed`(文件已带校验过的结果)、
`pending`(已启用且配置完整,可以评估)或 `unavailable`。agent 可以在没有配置模型时使用
事实型交接文件,但在运行 `oberth attn assess` 前必须检查 `attn.enabled`。
有异常时同时提供 `attn_revision`,它必须与文件中的 `state_revision`
相等,供 agent 在同一份 `status` 响应中核对文件是否仍对应当前事实。
未声明的监听不是异常:`status` 已经列出该端口及其属主,agent 没有启动它,也不该去停它。
核心文件是 `oberth.attn/v1`,包含 `event_id`、`state_revision`、`worktree_root`、
Expand Down Expand Up @@ -315,6 +318,7 @@ oberth attn assess --project "$PWD" \
| `observation.source` | `status` | 不是主采集器时回答本轮采集器;非空表示缺失的端口是未知,不是空闲。 |
| `machine[].listening_known` | `status` | `listening` 是否是观察到的;false 表示这张表看不到该端口的所有者。 |
| `attn_path` | `status` | 当前异常的派生交接文件;没有异常时缺省。 |
| `attn` | `status` | 这份交接现在能否被评估:`enabled`、固定 `reason`、`assessment`(`assessed`、`pending` 或 `unavailable`)。 |
| `services[].starting` | `status` | 正在启动的瞬时事实;不是 run,也不等于 `running`。 |
| `starting.phase` | `status` | `waiting_dependency`、`prepare` 或 `spawning`。 |
| `starting.waiting_on` | `status` | 仍在等的依赖;观察失败时保留上次已知集合。 |
Expand Down
4 changes: 2 additions & 2 deletions engine/internal/attn/model/enable.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package model

// `oberth attn enable` is the one step that turns the optional assessment on.
// `oberth attn config` is the one step that stores the optional assessment settings.
// It stores the key the person chose, proves the key works with a single
// redacted probe, and only then writes the owner-only settings file.
//
Expand Down Expand Up @@ -41,7 +41,7 @@ const (
KeyFromPrompt KeySourceKind = "prompt"
)

// EnableOptions is one `oberth attn enable` request.
// EnableOptions is one `oberth attn config` request.
type EnableOptions struct {
// Preset fills Endpoint and Model from a named pair of defaults. It is a
// convenience, not a provider restriction: explicit fields win, and an
Expand Down
2 changes: 1 addition & 1 deletion engine/internal/attn/model/enable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ func TestSendsSummaryCoversBothDirections(t *testing.T) {
if !strings.Contains(text, "Will be sent") || !strings.Contains(text, "Will never be sent") {
t.Fatalf("human summary = %q", text)
}
if !strings.Contains(text, "oberth attn preview") {
if !strings.Contains(text, "oberth attn assess --dry-run") {
t.Fatal("the human summary does not point at the exact body")
}
}
4 changes: 2 additions & 2 deletions engine/internal/attn/model/preview.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package model

// What `oberth attn preview` shows and what `oberth attn enable` prints afterwards.
// What `oberth attn assess --dry-run` shows and what `oberth attn config` prints afterwards.
// Both answer the same question in different tenses: "what exactly crosses the
// boundary?" A person enabling an optional remote assessment is entitled to a
// concrete answer, not a promise in a document.
Expand Down Expand Up @@ -139,7 +139,7 @@ func HumanSendsSummary() string {
writeBullets(&b, sendsNever)
b.WriteString("Depends on the event:\n")
writeBullets(&b, sendsDepends)
b.WriteString("\nRun `oberth attn preview` to see the exact body for the current event.")
b.WriteString("\nRun `oberth attn assess --dry-run` to see the exact body for the current event.")
return b.String()
}

Expand Down
8 changes: 4 additions & 4 deletions engine/internal/attn/model/reason.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ func ReasonFor(err error) string {
func ReasonHint(code string) string {
switch code {
case ReasonNoKey:
return "run `oberth attn enable` to store a key and turn the assessment on"
return "run `oberth attn config` to store a key, then `oberth attn on`"
case ReasonDisabled:
return "the assessment is off; run `oberth attn enable`, or set BERTH_JEV=1 to allow it for this one process"
return "the assessment is off; run `oberth attn on`, or set BERTH_ATTN=1 to allow it for this one process"
case ReasonInvalidConfig:
return "attn.json is not usable; run `oberth attn status` and then `oberth attn enable` to rewrite it"
return "attn.json is not usable; run `oberth attn status` and then `oberth attn config` to rewrite it"
case ReasonUnauthorized:
return "the provider rejected the key; run `oberth attn enable` to store a current one"
return "the provider rejected the key; run `oberth attn config` to store a current one"
case ReasonRateLimited:
return "the provider is rate limiting this key; retry later or switch provider"
case ReasonTimeout:
Expand Down
64 changes: 40 additions & 24 deletions engine/internal/cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,13 @@ type statusDocument struct {
// AttnRevision lets an agent compare an artifact it read with the
// exact deterministic facts represented by this status response.
AttnRevision string `json:"attn_revision,omitempty"`
// Attn says whether this handoff can be assessed right now. It is present
// exactly when AttnPath is, so path presence means "there is an anomaly",
// not "the person opted in".
Attn *statusAttn `json:"attn,omitempty"`
// attnEvent is whether the deterministic classifier derived a definite
// anomaly, independent of whether it was published. It never reaches the
// wire: consent decides the artifact, this decides the one-line offer of the
// enhancement, and reading consent off the artifact would make that offer
// unreachable precisely when it is due.
// anomaly. It never reaches the wire: it drives the one-line offer of the
// enhancement, which must stay reachable while the assessment is off.
attnEvent bool
// Changed is absent on the first run: there is nothing to compare against,
// which is not the same as nothing having changed (`empty` inside says
Expand Down Expand Up @@ -363,29 +365,33 @@ func buildStatusDocument(ctx context.Context, dir string, mark bool, only []stri
return doc, nil
}

// statusAttn is the consent and assessment state of the published handoff.
// Assessment is "assessed" when the artifact already carries a validated
// result, "pending" when the configuration could assess it now, and
// "unavailable" otherwise; Reason carries the fixed code when nothing can run.
type statusAttn struct {
Enabled bool `json:"enabled"`
Reason string `json:"reason,omitempty"`
Assessment string `json:"assessment"`
}

// publishAttention makes the deterministic anomaly visible to an optional
// agent adapter without putting a model or network call in status' read path.
// Failure to publish the optional artifact never makes the facts unavailable.
//
// The artifact only exists once the person has enabled the enhancement. An
// `attn_path` is therefore an unambiguous signal: an agent that sees one
// knows model is on and may run `oberth attn assess`, and an agent that sees none
// does not have to guess whether that is because nothing failed or because the
// user never opted in. Publishing it unconditionally meant every reader had to
// reconstruct that answer from the settings file.
//
// Deriving and publishing are two decisions, not one. The derivation is pure
// fact work and always runs, so `status` knows whether there is an anomaly even
// when it publishes nothing; only publication depends on consent. Collapsing
// them would leave the offer of the enhancement ("there is something to assess
// here") with no evidence to fire on, because the artifact it used to read is
// exactly what consent removes.
// The artifact is a local fact derived by code: publishing it sends nothing
// anywhere, so consent is not what decides whether it exists. An `attn_path`
// means "there is an anomaly"; the adjacent `attn` block says whether an
// assessment is possible and why not. Keeping the two decisions separate means
// an agent can use the fact-only handoff, and offer the enhancement, without
// the person having configured a model first.
func publishAttention(doc *statusDocument) string {
if doc == nil {
return ""
}
doc.AttnPath = ""
doc.AttnRevision = ""
doc.Attn = nil
doc.attnEvent = false
facts := attn.Facts{
WorktreeRoot: doc.Scope.Root,
Expand Down Expand Up @@ -462,22 +468,32 @@ func publishAttention(doc *statusDocument) string {
return ""
}
doc.attnEvent = true
if !attnEnabled() {
// The person has not opted in, so nothing is published. Clear a
// leftover artifact from before the enhancement was switched off:
// leaving it on disk would keep telling agents to assess.
_ = attn.Clear(paths.Dir(), doc.Scope.Root)
return ""
}
path, err := attn.Write(paths.Dir(), artifact)
if err != nil {
return ""
}
doc.AttnPath = path
doc.AttnRevision = artifact.StateRevision
doc.Attn = attnState(path)
return path
}

// attnState reports whether the published artifact can be assessed now. It
// reads consent for this process, never the artifact, and never contacts an
// endpoint.
func attnState(path string) *statusAttn {
consent := attnResolvedConsent()
state := &statusAttn{Enabled: consent.Enabled, Reason: consent.Reason, Assessment: "unavailable"}
if stored, err := attn.Read(path); err == nil && stored.Assessment != nil {
state.Assessment = "assessed"
return state
}
if consent.Enabled && consent.Reason == "" {
state.Assessment = "pending"
}
return state
}

// localStatusGroup is the cold-start fallback for a manifest the daemon has
// not indexed yet. It uses the same resolver and state conversion as the
// normal scan path, but does not pretend that a local read knows daemon run
Expand Down
Loading
Loading