[v0.1/Proto Review] SessionInfo.sequence の 0 sentinel と Attachment の時刻型を 1.0 で正規化 #144

Closed
opened 2026-09-05 11:57:26 +09:00 by natsukium · 3 comments
Owner

背景

felis.proto は 1.0 で wire 凍結するが、現行の SessionInfoAttachment に「0 sentinel が valid 値と衝突する」設計が残っている。minor 3 の ledger 行はこれを patch しているが、proto 上の型が optional 化されていないため、将来の breaking を避けるなら今しか直せない。

1. SessionInfo.sequence = 0 sentinel

message SessionInfo {
  // ...
  uint64 sequence = 12; // minor 3
}

sequence は daemon 起動後の monotonic な作成順 counter で、switch 環の順序に使う。minor 3 未満の daemon は field を書かず、proto3 の default 0 が読まれる。client は 0 を「ring は id 順に fallback」として扱う(reference/ipc.md The minor ledger 行3)。

問題: sequence1 起算だが、proto 上は uint64 (non-optional) で 0 が valid か sentinel か区別できない。将来「sequence 0 を valid に使う」ことはないとしても、proto 上の default と sentinel が同一であるため、ledger の「old peer writes none → reads 0」記述が型で保証されず、テストも 0 を sentinel として特別扱いする分岐に依存する。

同様に SessionInfo.attachments: repeated Attachment は minor 2 未満で空だが、これは repeated なので empty が自然に sentinel になる。一方 sequence は scalar のため optional 化しない限り sentinel を持てない。

2. Attachment.attached_at: string (RFC 3339)

message Attachment {
  uint64 id = 1;
  string attached_at = 2; // RFC 3339 UTC
  bool input_owner = 3;
}

attached_atreference/ipc.md で「daemon が持つ clock に対する絶対時刻」と定義され、相対秒では cross-host reader が再導出できないため string で持つ。実装は chrono/time で parse する。

問題: 文字列の RFC 3339 は parse 失敗で session list 全体が decode error になるリスクを持つ。proto 上で string は UTF-8 保証のみで、format を強制できない。GridMsg::PromptMark.line: uint64SessionInfo.idle_seconds: optional uint64 は数値で持っており、attached_at だけが文字列である非対称がある。Timestamp 型 (seconds + nanos) や uint64 epoch millis にすれば validation が型で閉じる。

3. SessionInfo.idle_seconds: optional uint64 の 0 意味

optional uint64 idle_seconds = 5; // 0 while attached

0 while attached とコメントにあるが、optional の absent と 0 の両方が「attached」を意味する二重表現になっている。minor 0 の base schema から optional だったため、0 sentinel を避けようとした痕跡だが、結果として「field があれば 0 も valid」になり、ledger の old peer 記述が曖昧。

問い

1.0 で以下を決める:

  1. sequenceoptional uint64 (または google.protobuf.UInt64Value wrapper) にし、absent = old daemon / 0 sentinel を廃止するか。1 起算の monotonic を 1.. の required にし、absent だけが fallback トリガになるようにするか。
  2. Attachment.attached_atstring のまま frozen にするか、Timestamp / uint64 epoch に正規化するか。protoc の well-known google.protobuf.Timestamp を使うか、felis 独自の uint64 attached_at_millis にするか。
  3. idle_seconds0 / absent 二重性を解消し、0 = attached、>0 = detached seconds に一本化するか。proto 上は non-optional uint64 にして 0 を attached とするか、optional のまま absent を attached とするか。

提案

  • 案A (最小 breaking): sequence のみ optional uint64 sequence = 12 にする。1.0 前の breaking として BREAKING.mdbase: <sha> を追加し、just proto-compat を green にする。client の fallback は None → 0 ではなく None → id 順 に変更し、0 を valid sequence として扱わないことを reference/ipc.md The minor ledger に追記。attached_atidle_seconds は 1.0 で frozen と明記し、将来の major で見直す Revisit trigger を explanation/architecture/ipc.md に残す。
  • 案B (完全正規化): sequence: optional uint64, attached_at: uint64 attached_at_unix_millis, idle_seconds: uint64 idle_seconds (non-optional, 0=attached) に 1.0 で一括変更。BREAKING.md に一括の base 行を追加。reference/ipc.md「Session (kind=4)」節の field 表を更新し、explanation/data-model に「なぜ int epoch か」を記録。

案A は sequence の optional 化だけを今やり、attached_at の string は実害が小さいため凍結する。案B は 3 field を一度に直すが、client の chrono parse が消える分、実装が簡潔になる。

判定基準

  • felis.protoSessionInfo.sequence が optional か否かで 1.0 の ledger 行「old daemon writes none」が型で保証されること。
  • Attachment.attached_at が string のままなら reference/ipc.md に「RFC 3339 固定、parse 失敗時の扱い」が凍結として明記されること。int にするなら BREAKING.md に base 行があり cargo test -p felis-protocol の round-trip が green であること。
  • idle_seconds0 / absent 意味が reference/ipc.md「Session (kind=4)」で一意に決まり、cli_output.rs の human 表示と json 出力が同じ解釈をすること。

対象ファイル

  • crates/felis-protocol/proto/felis.proto (SessionInfo.sequence, Attachment.attached_at, SessionInfo.idle_seconds)
  • crates/felis-protocol/src/messages.rs / convert/session.rs (SessionInfo, Attachment の domain 型)
  • crates/felis-protocol/src/preface.rs (MINOR_LEDGER の sequence 行)
  • docs/reference/ipc.md「Session (kind=4)」「The minor ledger」
  • docs/explanation/architecture/ipc.md「Session lifecycle」

Parent: #12 および #52 / #47

## 背景 `felis.proto` は 1.0 で wire 凍結するが、現行の `SessionInfo` と `Attachment` に「0 sentinel が valid 値と衝突する」設計が残っている。minor 3 の ledger 行はこれを patch しているが、proto 上の型が optional 化されていないため、将来の breaking を避けるなら今しか直せない。 ### 1. `SessionInfo.sequence = 0` sentinel ```proto message SessionInfo { // ... uint64 sequence = 12; // minor 3 } ``` `sequence` は daemon 起動後の monotonic な作成順 counter で、switch 環の順序に使う。minor 3 未満の daemon は field を書かず、proto3 の default `0` が読まれる。client は `0` を「ring は id 順に fallback」として扱う(`reference/ipc.md` The minor ledger 行3)。 問題: `sequence` は `1` 起算だが、proto 上は `uint64` (non-optional) で `0` が valid か sentinel か区別できない。将来「sequence 0 を valid に使う」ことはないとしても、proto 上の default と sentinel が同一であるため、ledger の「old peer writes none → reads 0」記述が型で保証されず、テストも `0` を sentinel として特別扱いする分岐に依存する。 同様に `SessionInfo.attachments: repeated Attachment` は minor 2 未満で空だが、これは repeated なので empty が自然に sentinel になる。一方 `sequence` は scalar のため optional 化しない限り sentinel を持てない。 ### 2. `Attachment.attached_at: string` (RFC 3339) ```proto message Attachment { uint64 id = 1; string attached_at = 2; // RFC 3339 UTC bool input_owner = 3; } ``` `attached_at` は `reference/ipc.md` で「daemon が持つ clock に対する絶対時刻」と定義され、相対秒では cross-host reader が再導出できないため string で持つ。実装は `chrono`/`time` で parse する。 問題: 文字列の RFC 3339 は parse 失敗で session list 全体が decode error になるリスクを持つ。proto 上で `string` は UTF-8 保証のみで、format を強制できない。`GridMsg::PromptMark.line: uint64` や `SessionInfo.idle_seconds: optional uint64` は数値で持っており、`attached_at` だけが文字列である非対称がある。`Timestamp` 型 (seconds + nanos) や `uint64` epoch millis にすれば validation が型で閉じる。 ### 3. `SessionInfo.idle_seconds: optional uint64` の 0 意味 ```proto optional uint64 idle_seconds = 5; // 0 while attached ``` `0 while attached` とコメントにあるが、`optional` の absent と `0` の両方が「attached」を意味する二重表現になっている。minor 0 の base schema から optional だったため、0 sentinel を避けようとした痕跡だが、結果として「field があれば `0` も valid」になり、ledger の old peer 記述が曖昧。 ## 問い 1.0 で以下を決める: 1. `sequence` を `optional uint64` (または `google.protobuf.UInt64Value` wrapper) にし、absent = old daemon / `0` sentinel を廃止するか。`1` 起算の monotonic を `1..` の required にし、absent だけが fallback トリガになるようにするか。 2. `Attachment.attached_at` を `string` のまま frozen にするか、`Timestamp` / `uint64` epoch に正規化するか。`protoc` の well-known `google.protobuf.Timestamp` を使うか、felis 独自の `uint64 attached_at_millis` にするか。 3. `idle_seconds` の `0` / absent 二重性を解消し、`0` = attached、`>0` = detached seconds に一本化するか。proto 上は non-optional `uint64` にして `0` を attached とするか、optional のまま absent を attached とするか。 ## 提案 - **案A (最小 breaking)**: `sequence` のみ `optional uint64 sequence = 12` にする。1.0 前の breaking として `BREAKING.md` に `base: <sha>` を追加し、`just proto-compat` を green にする。client の fallback は `None → 0` ではなく `None → id 順` に変更し、`0` を valid sequence として扱わないことを `reference/ipc.md` The minor ledger に追記。`attached_at` と `idle_seconds` は 1.0 で frozen と明記し、将来の major で見直す Revisit trigger を `explanation/architecture/ipc.md` に残す。 - **案B (完全正規化)**: `sequence: optional uint64`, `attached_at: uint64 attached_at_unix_millis`, `idle_seconds: uint64 idle_seconds` (non-optional, 0=attached) に 1.0 で一括変更。`BREAKING.md` に一括の base 行を追加。`reference/ipc.md`「Session (kind=4)」節の field 表を更新し、`explanation/data-model` に「なぜ int epoch か」を記録。 案A は `sequence` の optional 化だけを今やり、`attached_at` の string は実害が小さいため凍結する。案B は 3 field を一度に直すが、client の `chrono` parse が消える分、実装が簡潔になる。 ## 判定基準 - `felis.proto` の `SessionInfo.sequence` が optional か否かで 1.0 の ledger 行「old daemon writes none」が型で保証されること。 - `Attachment.attached_at` が string のままなら `reference/ipc.md` に「RFC 3339 固定、parse 失敗時の扱い」が凍結として明記されること。int にするなら `BREAKING.md` に base 行があり `cargo test -p felis-protocol` の round-trip が green であること。 - `idle_seconds` の `0` / absent 意味が `reference/ipc.md`「Session (kind=4)」で一意に決まり、`cli_output.rs` の human 表示と `json` 出力が同じ解釈をすること。 ## 対象ファイル - `crates/felis-protocol/proto/felis.proto` (`SessionInfo.sequence`, `Attachment.attached_at`, `SessionInfo.idle_seconds`) - `crates/felis-protocol/src/messages.rs` / `convert/session.rs` (`SessionInfo`, `Attachment` の domain 型) - `crates/felis-protocol/src/preface.rs` (`MINOR_LEDGER` の sequence 行) - `docs/reference/ipc.md`「Session (kind=4)」「The minor ledger」 - `docs/explanation/architecture/ipc.md`「Session lifecycle」 Parent: #12 および #52 / #47
Author
Owner

Triage (2026-09-05)

Verdict: decision needed, P1. Recommendation: freeze the wire as-is (neither option A nor B), do the docs half.

  • sequence: after v0.1.0 every peer writes sequence >= 1; the 0 fallback describes pre-release daemons no release will meet. Making it optional buys a type-level statement about peers that do not exist.
  • attached_at: the value is daemon-produced, so a parse failure is a daemon bug, not an input-validation gap; RFC 3339 stays.
  • idle_seconds: a wording fix. State one meaning for absent and 0 in reference/ipc.md and make cli_output.rs follow it.

If accepted: record the three semantics and a Revisit trigger in the same reference/ipc.md Session table #146 adds, then close this issue as folded into #146. If the user wants option A instead, it needs a base: line in BREAKING.md and must land before #50 and #29. Parent: #52.

## Triage (2026-09-05) **Verdict: decision needed, P1. Recommendation: freeze the wire as-is (neither option A nor B), do the docs half.** - `sequence`: after v0.1.0 every peer writes `sequence >= 1`; the `0` fallback describes pre-release daemons no release will meet. Making it `optional` buys a type-level statement about peers that do not exist. - `attached_at`: the value is daemon-produced, so a parse failure is a daemon bug, not an input-validation gap; RFC 3339 stays. - `idle_seconds`: a wording fix. State one meaning for absent and `0` in `reference/ipc.md` and make `cli_output.rs` follow it. If accepted: record the three semantics and a Revisit trigger in the same `reference/ipc.md` Session table #146 adds, then close this issue as folded into #146. If the user wants option A instead, it needs a `base:` line in `BREAKING.md` and must land before #50 and #29. Parent: #52.
Author
Owner

最小breakingにこだわる必要はない

最小breakingにこだわる必要はない
Author
Owner

Triage plan (2026-09-05)

Source-grounded triage against main at 07a77ae4, reviewed through six rounds of an independent reviewer (pi luna then sol); every finding was verified against the source and folded in, and the design decisions below were settled with the maintainer on 2026-09-05. The order that supersedes the tracker's is posted on #12: this issue lands before #29 (schemas, goldens, and reference tables freeze there) and, because it breaks the wire, before #50.

SessionInfo.sequence, Attachment.attached_at, idle_seconds

Claim check

  • felis.proto:625-630 uint64 sequence = 12 (minor 3); the client ring
    (felis-client-core/src/roster.rs:7-32,86) orders by sequence with id
    as tiebreak, so "every row reads 0 → id order" is the implicit fallback
    the ledger row 3 (ipc.md:2118) describes.
  • attached_at: produced by crates/felis-daemon/src/timestamp.rs
    (rfc3339_utc(SystemTime::now()), session_task.rs:1180); the CLI
    passes the string through (cli_output.rs:537,571, cli_sessions.rs:1038);
    no parse on the client side, so the "decode error" risk in the issue is
    overstated (it is a string field; nothing parses it).
  • idle_seconds: optional uint64 idle_seconds = 5; // 0 while attached
    — the comment defines 0 and leaves absent undefined.
  • Gate: crates/felis-protocol/proto/BREAKING.md holds first-release
    and no base: line; a wire break needs a base: <sha> line and just proto-compat green (scripts/proto/compat.sh).

Verdict

accept, option B (normalize on the wire), per the maintainer's note
"最小 breaking にこだわる必要はない".
One base: line; lands before #50
(which turns the ledger into executable metadata and would otherwise
encode the old fields) and #29.

Decision (attached_at type) — settled 2026-09-05: google.protobuf.Timestamp.
The maintainer allows the import, so the well-known type wins over a
felis-private integer: it is the typed, range-defined (years 0001-9999,
nanosecond precision) representation every protobuf consumer already
has a native mapping for, which is what the cross-language reuse surface
wants. felis.proto gains import "google/protobuf/timestamp.proto"
and felis-protocol gains prost-types (pure, no OS or tokio
dependency, so the purity rule holds; tests/crate_purity.rs should
still pass). Rejected: uint64 attached_at_unix_ms (a private
convention with a hand-written range cap) and keeping the string
(format enforced by nothing). The CLI surface does not change:
--format json keeps attached_at as an RFC 3339 string and human
output renders the same; the formatter (crates/felis-daemon/src/timestamp.rs)
moves to felis-cli, its only remaining user, and the daemon fills the
Timestamp from SystemTime via prost_types::Timestamp::from.
Decode rejects a Timestamp outside the type's documented range or with
nanos outside 0..1e9 as MalformedField.

Approach

  • felis.proto: optional uint64 sequence = 12 (absent = the daemon
    predates minor 3; the ring falls back to id order on None, never on
    0; 1-based stays). Model it as Option<NonZeroU64> in
    messages/ops.rs so convert/ops.rs rejects Some(0) as
    MalformedField (a REQ-114 connection failure) instead of admitting
    the retired sentinel into the ring; decode tests for None, Some(1),
    and rejected Some(0); Attachment.attached_at becomes google.protobuf.Timestamp in field 2
    (reserve the old name); idle_seconds stays optional uint64 with the
    comment rewritten: absent = attached, present = seconds since the last
    detach (0 = under a second). The daemon writes Some(0) while attached today
    (crates/felis-daemon/src/serve.rs:2422-2426); change it to None, so
    presence alone says "detached" and 0 regains its one meaning.
  • messages/ops.rs / convert/ops.rs (where SessionInfo and
    Attachment convert; not convert/session.rs): sequence: Option<NonZeroU64> everywhere in the domain (project with get() only
    at the ring key),
    attached_at: Timestamp (domain type wrapping SystemTime or the
    prost type); just proto to regenerate the committed
    prost output; roster: RingKey.sequence: Option<NonZeroU64> — next/previous
    order Some by sequence with None after them by id, and the
    shell-exit pick (roster.rs:86, abs_diff on the sequences today)
    uses sequence distance only when both anchor and candidate are Some,
    else the existing id-order fallback; tests for both walks and the
    exit pick with None on each side;
    cli_output.rs / cli_sessions.rs:1009,1038: human rendering.
  • BREAKING.md: one base: 07a77ae4… line with the why. Expect buf breaking (WIRE_JSON) to report the attached_at name/type/JSON-name
    change and sequence's implicit → explicit presence; record the actual
    diagnostics in the ack's prose, and name the wire-silent idle_seconds
    semantics there too. Skew posture, stated in BREAKING.md
    and CHANGELOG.md: this is a pre-release semantic break on the 1.9
    tree (the #138 decision makes 1.9 the first public baseline); a daemon
    and client from either side of it must be rebuilt together, and no
    skew handling is written. State the actual skew outcome: field 2 of
    Attachment changes wire type (string → varint), so prost rejects the
    mismatch (string → embedded message is a wire-type change only for
    consumers that read the old string; prost decodes a length-delimited
    message where it expects a string as a UTF-8 failure or a garbage
    string) and any roster carrying an attachment is a decode failure that
    ends the connection in either direction; an old daemon's Some(0)
    idle would read as "detached" where decoding succeeds. Not a minor
    bump: no released peer exists to negotiate with.
  • Consumers to update, all found by the compiler or by grep for
    sequence: 0 / idle_seconds: Some(0) (13 sites in crates/felis-cli,
    including cli_completions.rs fixtures): roster.rs key type
    (Option<u64>, None sorts by id), cli_output.rs:486-571,
    cli_sessions.rs:1009,1038, and docs/how-to/reap-sessions.md:25-31,
    which documents idle_seconds == 0 as "attached" and must switch to
    "absent" (select(.idle_seconds != null and .idle_seconds > 3600)).
  • The RFC 3339 formatter (crates/felis-daemon/src/timestamp.rs) moves
    to felis-cli (the dependency direction forbids the CLI depending on
    the daemon); the daemon no longer formats anything.
  • docs/reference/ipc.md "Session (kind = 4)": a field table row per
    field with the frozen semantics; ledger row 2 (ipc.md:2117, names
    attached_at) and row 3 ("writes none → absent") both updated;
    docs/explanation/architecture/ipc.md "Session lifecycle": why integer
    Timestamp (typed, range-defined, no format to validate) and why sequence is optional
    (presence, not a sentinel, says "old daemon");
    docs/explanation/architecture/session-lifecycle.md: the switch-ring
    rationale that names the 0 fallback is rewritten for None.
  • docs/reference/cli.md machine output for sessions list/info is
    unchanged (attached_at RFC 3339 string); skills/felis unchanged;
    CHANGELOG.md for the wire change and the idle_seconds semantics.
  • Tests: proto round-trip; roster fallback on None; CLI JSON golden for
    an attachment; just proto-compat with the ack.

Dependencies / risk / labels

Before #50 and #29. M (touches daemon, protocol, client-core, CLI,
docs; mechanical). Keep labels. Parent #52.

## Triage plan (2026-09-05) Source-grounded triage against `main` at `07a77ae4`, reviewed through six rounds of an independent reviewer (`pi` luna then sol); every finding was verified against the source and folded in, and the design decisions below were settled with the maintainer on 2026-09-05. The order that supersedes the tracker's is posted on #12: this issue lands before #29 (schemas, goldens, and reference tables freeze there) and, because it breaks the wire, before #50. **`SessionInfo.sequence`, `Attachment.attached_at`, `idle_seconds`** ### Claim check - `felis.proto:625-630` `uint64 sequence = 12` (minor 3); the client ring (`felis-client-core/src/roster.rs:7-32,86`) orders by `sequence` with id as tiebreak, so "every row reads 0 → id order" is the implicit fallback the ledger row 3 (`ipc.md:2118`) describes. - `attached_at`: produced by `crates/felis-daemon/src/timestamp.rs` (`rfc3339_utc(SystemTime::now())`, `session_task.rs:1180`); the CLI passes the string through (`cli_output.rs:537,571`, `cli_sessions.rs:1038`); no parse on the client side, so the "decode error" risk in the issue is overstated (it is a string field; nothing parses it). - `idle_seconds`: `optional uint64 idle_seconds = 5; // 0 while attached` — the comment defines `0` and leaves absent undefined. - Gate: `crates/felis-protocol/proto/BREAKING.md` holds `first-release` and no `base:` line; a wire break needs a `base: <sha>` line and `just proto-compat` green (`scripts/proto/compat.sh`). ### Verdict **accept, option B (normalize on the wire), per the maintainer's note "最小 breaking にこだわる必要はない".** One `base:` line; lands before #50 (which turns the ledger into executable metadata and would otherwise encode the old fields) and #29. **Decision (attached_at type) — settled 2026-09-05: `google.protobuf.Timestamp`.** The maintainer allows the import, so the well-known type wins over a felis-private integer: it is the typed, range-defined (years 0001-9999, nanosecond precision) representation every protobuf consumer already has a native mapping for, which is what the cross-language reuse surface wants. `felis.proto` gains `import "google/protobuf/timestamp.proto"` and `felis-protocol` gains `prost-types` (pure, no OS or tokio dependency, so the purity rule holds; `tests/crate_purity.rs` should still pass). Rejected: `uint64 attached_at_unix_ms` (a private convention with a hand-written range cap) and keeping the string (format enforced by nothing). The CLI surface does not change: `--format json` keeps `attached_at` as an RFC 3339 string and human output renders the same; the formatter (`crates/felis-daemon/src/timestamp.rs`) moves to `felis-cli`, its only remaining user, and the daemon fills the `Timestamp` from `SystemTime` via `prost_types::Timestamp::from`. Decode rejects a `Timestamp` outside the type's documented range or with `nanos` outside `0..1e9` as `MalformedField`. ### Approach - `felis.proto`: `optional uint64 sequence = 12` (absent = the daemon predates minor 3; the ring falls back to id order on `None`, never on `0`; `1`-based stays). Model it as `Option<NonZeroU64>` in `messages/ops.rs` so `convert/ops.rs` rejects `Some(0)` as `MalformedField` (a REQ-114 connection failure) instead of admitting the retired sentinel into the ring; decode tests for `None`, `Some(1)`, and rejected `Some(0)`; `Attachment.attached_at` becomes `google.protobuf.Timestamp` in field 2 (reserve the old name); `idle_seconds` stays `optional uint64` with the comment rewritten: absent = attached, present = seconds since the last detach (`0` = under a second). The daemon writes `Some(0)` while attached today (`crates/felis-daemon/src/serve.rs:2422-2426`); change it to `None`, so presence alone says "detached" and `0` regains its one meaning. - `messages/ops.rs` / `convert/ops.rs` (where `SessionInfo` and `Attachment` convert; not `convert/session.rs`): `sequence: Option<NonZeroU64>` everywhere in the domain (project with `get()` only at the ring key), `attached_at: Timestamp` (domain type wrapping `SystemTime` or the prost type); `just proto` to regenerate the committed prost output; roster: `RingKey.sequence: Option<NonZeroU64>` — next/previous order `Some` by sequence with `None` after them by id, and the shell-exit pick (`roster.rs:86`, `abs_diff` on the sequences today) uses sequence distance only when both anchor and candidate are `Some`, else the existing id-order fallback; tests for both walks and the exit pick with `None` on each side; `cli_output.rs` / `cli_sessions.rs:1009,1038`: human rendering. - `BREAKING.md`: one `base: 07a77ae4…` line with the why. Expect `buf breaking` (WIRE_JSON) to report the `attached_at` name/type/JSON-name change and `sequence`'s implicit → explicit presence; record the actual diagnostics in the ack's prose, and name the wire-silent `idle_seconds` semantics there too. Skew posture, stated in `BREAKING.md` and `CHANGELOG.md`: this is a pre-release semantic break on the 1.9 tree (the #138 decision makes 1.9 the first public baseline); a daemon and client from either side of it must be rebuilt together, and no skew handling is written. State the actual skew outcome: field 2 of `Attachment` changes wire type (string → varint), so prost rejects the mismatch (string → embedded message is a wire-type change only for consumers that read the old string; prost decodes a length-delimited message where it expects a string as a UTF-8 failure or a garbage string) and any roster carrying an attachment is a decode failure that ends the connection in either direction; an old daemon's `Some(0)` idle would read as "detached" where decoding succeeds. Not a minor bump: no released peer exists to negotiate with. - Consumers to update, all found by the compiler or by grep for `sequence: 0` / `idle_seconds: Some(0)` (13 sites in `crates/felis-cli`, including `cli_completions.rs` fixtures): `roster.rs` key type (`Option<u64>`, `None` sorts by id), `cli_output.rs:486-571`, `cli_sessions.rs:1009,1038`, and `docs/how-to/reap-sessions.md:25-31`, which documents `idle_seconds == 0` as "attached" and must switch to "absent" (`select(.idle_seconds != null and .idle_seconds > 3600)`). - The RFC 3339 formatter (`crates/felis-daemon/src/timestamp.rs`) moves to `felis-cli` (the dependency direction forbids the CLI depending on the daemon); the daemon no longer formats anything. - `docs/reference/ipc.md` "Session (kind = 4)": a field table row per field with the frozen semantics; ledger row 2 (`ipc.md:2117`, names `attached_at`) and row 3 ("writes none → absent") both updated; `docs/explanation/architecture/ipc.md` "Session lifecycle": why integer `Timestamp` (typed, range-defined, no format to validate) and why `sequence` is optional (presence, not a sentinel, says "old daemon"); `docs/explanation/architecture/session-lifecycle.md`: the switch-ring rationale that names the `0` fallback is rewritten for `None`. - `docs/reference/cli.md` machine output for `sessions list/info` is unchanged (`attached_at` RFC 3339 string); `skills/felis` unchanged; `CHANGELOG.md` for the wire change and the `idle_seconds` semantics. - Tests: proto round-trip; roster fallback on `None`; CLI JSON golden for an attachment; `just proto-compat` with the ack. ### Dependencies / risk / labels Before #50 and #29. **M** (touches daemon, protocol, client-core, CLI, docs; mechanical). Keep labels. Parent #52.
Sign in to join this conversation.
No description provided.