Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2c81226
app tests: B2 recovery must show its worker's answer, not only a sett…
AbirAbbas Nov 11, 2026
ea62e91
Typing into your own idle chat reopens it instead of saying it's avai…
AbirAbbas Nov 11, 2026
308d65d
app: answered receipts and quiet notices disappear after relaunch (#2…
AbirAbbas Nov 11, 2026
00ad637
A big move from the hosted relay goes through; a move sync answered w…
AbirAbbas Nov 11, 2026
68382af
tests: FIFO and Unix-helper cases build for Windows; make vet checks …
AbirAbbas Nov 11, 2026
3ce443e
session: split the task and memory roads back under the endings ceili…
AbirAbbas Nov 11, 2026
c9f1c78
Book a tool completion once when the process dies between seal and WA…
AbirAbbas Nov 11, 2026
b318724
Keep waiting job actions from going stale on bookkeeping (#2463)
AbirAbbas Nov 11, 2026
249fd10
factory: keep close revision checks strict after action history merge
AbirAbbas Nov 11, 2026
3eeb3bd
Spending: daily rollup for date-only spend.report (#2269)
AbirAbbas Nov 11, 2026
19c0ecd
Retain Library activity in the rollup byte budget during replay
AbirAbbas Nov 11, 2026
cb81b6d
Keep billing facts and missing receipts when taking a chat (#2386)
AbirAbbas Nov 11, 2026
eed33ff
Retain the Library receipt helper while replaying the billing merge
AbirAbbas Nov 11, 2026
0448c32
session: retain Library activity on sealed missing-receipt rows
AbirAbbas Nov 11, 2026
21ebed3
An idle job chat stops reading every job ten times a second (#2358)
AbirAbbas Nov 11, 2026
08be528
Set the explicit Memory-off premise in the idle discovery fixture
AbirAbbas Nov 11, 2026
c25dbd3
agentdid: add encrypted custody, purpose-bound signing and v1 identit…
AbirAbbas Nov 11, 2026
61d259d
Idle factory workers wait for changes instead of re-decoding the stor…
AbirAbbas Nov 11, 2026
a3af916
store: distinguish file identity stamps from shared cache stamps
AbirAbbas Nov 11, 2026
6f9eed9
factory: bind retained caches to complete file identity and avoid dup…
AbirAbbas Nov 11, 2026
cf7a890
Count unchanged-floor settlement passes without decoding jobs
AbirAbbas Nov 11, 2026
26c4133
Recover usage rows after a crash without changing charges (#2387)
AbirAbbas Nov 11, 2026
107058f
session: keep journal appends in the usage funnel and back off by delay
AbirAbbas Nov 11, 2026
a3ab916
Crossing the day cap stops running work, not only the next turn (#2364)
AbirAbbas Nov 11, 2026
ba59092
session: sample the opening job budget before idle admission
AbirAbbas Nov 11, 2026
97bdc72
Factory: compare failing Go tests with the base, and Accept as is (#2…
AbirAbbas Nov 11, 2026
dc6bbde
factory: retain strict revision guard when pausing recovery
AbirAbbas Nov 11, 2026
122fb14
app: release the recovery-chat host lock through defer
AbirAbbas Nov 11, 2026
7b9ada6
Keep explicit test completion rules and conflict ownership
AbirAbbas Nov 11, 2026
70e1818
docs: record the local app/v3 integration wave
AbirAbbas Nov 11, 2026
a4ef5dc
tests: open the Knowledge offer with Memory explicitly off
AbirAbbas Nov 11, 2026
4fadc94
session: let the journal own projection durability until compaction
AbirAbbas Nov 11, 2026
7d7df56
tests: join routed receipt settlement before inspecting its charge
AbirAbbas Nov 11, 2026
309e9df
tests: request the job-vault discovery passes that the fixture checks
AbirAbbas Nov 11, 2026
9dd276d
tests: arm the admission ledger probe after the startup snapshot
AbirAbbas Nov 11, 2026
9b23108
fix(session): preserve free helper admission through shared daily gat…
AbirAbbas Nov 11, 2026
306a14c
fix(factory): compare pinned failures through the command fence (#2570)
AbirAbbas Nov 11, 2026
250a5ea
Merge dev (#2595, #2596) into the app/v3 open-PR wave
AbirAbbas Nov 11, 2026
980918d
changes: the app/v3 open-PR wave names its pull request
AbirAbbas Nov 11, 2026
09dbdb2
test(sync): pair shared fixtures before #2090 custody saves a solo id…
AbirAbbas Nov 11, 2026
e8fc2e3
fix(factory): invalidate #2358 handover look before stale admission
AbirAbbas Nov 11, 2026
0d20225
test(factory): keep #2570 belt fixture outside its profile ancestor
AbirAbbas Nov 11, 2026
e62b4ef
fix(session): keep #2387 journal sync in its process writer scope
AbirAbbas Nov 11, 2026
c3637de
fix(session): route #2570 base-sync arguments through the shared decoder
AbirAbbas Nov 11, 2026
39089c3
test(session): initialize stalled journal fixtures through the writer…
AbirAbbas Nov 11, 2026
1ac7f8a
test(app): end the stray child portably so the Windows test build vet…
AbirAbbas Nov 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
85 changes: 85 additions & 0 deletions .github/workflows/agent-identity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Agent identity acceptance

on:
pull_request:
branches: [app/v3]
paths:
- internal/agentdid/**
- internal/session/agent_identity*.go
- internal/session/place.go
- internal/session/session.go
- internal/session/approvalposture.go
- internal/session/effort.go
- cmd/codeaf/syncdrive_test.go
- cmd/codeaf/heldchat_drive_test.go
- cmd/codeaf/app_agent_identity_e2e_test.go
- .github/workflows/agent-identity.yml

permissions:
contents: read

concurrency:
group: agent-identity-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
binary-e2e:
name: Binary user-flow QA
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Build the actual app binary
run: make build
- name: Exercise the app as a desktop client
run: go test ./cmd/codeaf -run '^TestAppAgentIdentityBinaryE2E$' -count=1 -v -timeout 5m
- name: Check that custody preserves existing sync behavior
run: go test ./cmd/codeaf -run '^TestAQuitAtTheMoveQuestionLeavesTheOtherWindowsChatSendable$|^TestTheFreshChatBesideAHeldOneWorksOnItsOwnSeat$|^TestDriveSideDoorPublishesAndReleases$|^TestDriveSideStartsWhenAPairingMakesTheIdentity$|^TestAChatMovedBackGetsADriveSideOfItsOwn$|^TestAChatTakenAndTakenBackGetsADriveSideOfItsOwn$|^TestDriveSideStartsWhenTheSoloMarkerEnds$|^TestAppOrdinaryClientIDSurvivesABackendRestart$|^TestC6AppSendWithoutAViewerStaysLocal$|^TestC27NewChatByDefaultStaysQuiet$' -count=1 -v -timeout 5m

- name: Repeat sealed history portability
run: go test ./internal/session -run '^TestSealedTreeCarriesTaskHistory$' -count=20 -v -timeout 5m

race:
name: Custody race checks
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Race-check custody and lifecycle
run: go test -race ./internal/agentdid ./internal/session -run 'TestCredentials|TestCustody|TestOnlyTheRecipient|TestUnknownDocument|TestAgentIdentity|TestAgentDID|TestToolCommandsSeeTheAgentDID|TestTaskWorker.*DID' -count=1 -timeout 15m

repository:
name: Repository gates
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Build with the pinned engine
run: make build
- name: Run the repository PR gates against app/v3
env:
BASE: ${{ github.event.pull_request.base.sha }}
run: make pr-ready
- name: Run touched packages even if an existing light gate fails
if: ${{ !cancelled() }}
env:
BASE: ${{ github.event.pull_request.base.sha }}
run: make test-touched
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jobs:
run: go build ./...

- name: Vet
run: go vet ./...
run: make vet

# A file gofmt would rewrite is a file the next editor's save rewrites,
# and that diff lands in somebody else's pull request. Two files had
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,14 @@ demo-home: build
clean-run: build
@scripts/clean-run.sh

# VET ALSO COMPILES THE TESTS FOR WINDOWS. `go build` never compiles a test file,
# so a test that calls a Unix-only symbol (syscall.Mkfifo, a helper from a
# _unix.go file) breaks the Windows test build without any gate seeing it. The
# Windows pass type-checks the same packages with GOOS=windows; it costs about
# 30 seconds on a cold cache and 2 on a warm one.
vet:
go vet ./...
GOOS=windows go vet ./...

# ── the size ratchet ────────────────────────────────────────────────────────
#
Expand Down
447 changes: 447 additions & 0 deletions NOTES-FIX-2597.md

Large diffs are not rendered by default.

45 changes: 30 additions & 15 deletions PERF.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ repair, which competes for the same finite capacity; returning to monitoring
releases it. The capacity, queue projection and monitoring-lease regressions in
`internal/factory/serve`, `lease` and `worker` defend this separation.

## Unchanged factory listing

`TestListDoesNotDecodeAnUnchangedFloor` permits at most **100 allocations**
for a cached one-item listing. It allows stat metadata, including inode and
change time, while rejecting another JSON decode. Chmod changes ctime and
must invalidate the cache, so changing permissions cannot simulate an
unchanged document.

## Tool process identity in history

`DisplayEntry` is bounded at **328 bytes** on a 64-bit machine. The process
Expand Down Expand Up @@ -416,6 +424,14 @@ The writer batches at most 128 already admitted rows per ledger lock. A quiet
queue flushes immediately, and flush/close barriers follow completed writes.
Identity generation checks and duplicate suppression still run under that lock.

The writer owns a separate journal loop that waits for coalesced admission
wakes while idle. It starts batch synchronizers in the process writer's clock
scope, independently of a blocked ledger projection, and joins the final sync
before recovery and shutdown. `TestJournalSyncStaysWithTheProcessWriterAcrossCallerScopes`
records and joins from a shorter caller scope, then checks the public ledger's
single unchanged charge. The journal interval and row threshold remain
`usageSyncInterval` and `usageSyncRows`.

## Factory signal acknowledgement bounds

Each worker heartbeat sends at most 256 unconfirmed signal delivery IDs. A failed
Expand All @@ -440,17 +456,12 @@ to leave every waiting frame file untouched on a repeat.

## Idle factory manager checks

Every open job manager and every open step asks whether its job was stopped or
its budget reached (`watchJobBudget` in `internal/session/job_budget.go`): ten
times a second (`jobBudgetLiveEvery`) while it owns work in flight — a turn, a
running background command, an unfinished task — and once every five seconds
(`jobBudgetIdleEvery`, the presence beat) while it owns none, since there is
then nothing a Stop or a spent budget could cut short and the next model call
is checked at its own boundary. An idle watch sleeps that whole interval on one
timer rather than waking every 100 ms; a turn opening, a command starting and
every presence nudge wake it onto the live cadence at once (`wakeWorkWatch`).
`TestIdleManagerBudgetWatchLooksOnceABeat`: an idle manager looks at most once
in 950 ms, and at least three times in 500 ms once a command runs.
Every open job manager checks its saved job budget once when opened. Thereafter
`watchJobBudget` polls at `budgetPollEvery` (100 ms) only while capped model
calls, commands or delegated work are moving. An idle manager holds no timer;
new work wakes the watch and each model boundary checks admission again.
`TestIdleManagerBudgetWatchLooksOnceABeat` retains the initial-check and active-work
bounds. Daily cap detection follows settled ledger writes, within one active poll.

Neither check may decode a job document that has not changed since it last
looked. `seamRunDoor.AutomaticRefusal` (`cmd/codeaf/factory_person_stop.go`)
Expand All @@ -475,8 +486,12 @@ floor folder: the folder's time moves with every file renamed into it (the
runner registry, a lease heartbeat, the sources' poll record), which made every
open step chat list and decode every job every few seconds. It is answered from
one look at the floor's job files (`floorJobFiles`: one directory read and one
stat per job, shared by every conversation for `floorLookEvery`, 100 ms) and
reads only a job document that changed. `TestAStepChatsStopCheckDecodesNothingWhileTheFloorChurns`:
stat per job, shared by every conversation for `floorLookEvery`, 100 ms while
the directory is unchanged) and reads only a job document that changed. Each
admission stats the directory before reusing that look, so an atomic job rename
invalidates it immediately. `TestAutomaticRefusalSeesHandoverBeforeNextFloorInterval`
freezes the shared clock and requires a newly assigned stopped job to refuse
the next admission. `TestAStepChatsStopCheckDecodesNothingWhileTheFloorChurns`:
fifty checks while the registry is rewritten decode none (`factoryStopCheckDecodes`);
one changed job costs one decode, and a job that takes the conversation is seen
on the next look.
Expand Down Expand Up @@ -520,7 +535,7 @@ system call between its heartbeats (`internal/session/taskpresence.go`):
shared directory watch reports a write there (`presenceDoorstepEvent`), and
otherwise once a minute (`presenceQuietPass`); a folder the watch cannot see
is knocked on every five-second beat as before.
- Its job's budget watch (above) sleeps five seconds at a time.
- Its job's budget watch (above) holds no timer while idle.

`TestAnIdleChatWritesNothingBetweenHeartbeats` drives one desk on fake shared
clocks through a minute of beats and refreshes and fails on any rewrite before
Expand All @@ -546,7 +561,7 @@ a second is named on stderr with its two halves:
`store.Store.read` (every `Get`, `List`, `ListSaved` and update of a job
document) decodes a document only when its file changed since the process last
read it (`internal/factory/store/readcache.go`): one stat, compared on
`os.SameFile`, size, modification time and mode — every store write renames a
`os.SameFile`, size, modification time, change time, inode and mode — every store write renames a
fresh file into place — answers an unchanged document from a process-wide
cache, deep-copied (`deepCopy`) so no caller shares anything with the next. The
cache is the process's, not a `Store`'s, since many doors open their own store
Expand Down
7 changes: 3 additions & 4 deletions bench/conversation/lib/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,10 @@ def read_codeaf_home(home, stdout_path):
line: they are what an open-model law has to be enforced against."""
got = blank()
usage_path = os.path.join(home, "v3", "usage.jsonl")
rows = list(jsonl(usage_path))
rows = [row for row in jsonl(usage_path) if row.get("kind") != "unbilled-marker"]
if not rows:
# A session that never reached the wire leaves no usage file at all.
# That is an absence of measurement, not a free run.
got["notes"].append("no v3/usage.jsonl under the cell home")
# Missing receipts establish no usage measurement or zero price.
got["notes"].append("no measured calls in v3/usage.jsonl under the cell home")
cost = 0.0
tokens_in = tokens_out = 0
models, aux_models = [], []
Expand Down
38 changes: 38 additions & 0 deletions bench/conversation/test/test_billing_marker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Missing receipts are accounting markers, not measured model calls."""
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest

spec = importlib.util.spec_from_file_location("receipts", Path(__file__).parents[1] / "lib/receipts.py")
receipts = importlib.util.module_from_spec(spec)
spec.loader.exec_module(receipts)


class BillingMarkerTests(unittest.TestCase):
def test_home_reader_skips_missing_receipt_markers(self):
with tempfile.TemporaryDirectory() as home:
ledger = Path(home) / "v3/usage.jsonl"
ledger.parent.mkdir()
rows = [{"kind": "unbilled-marker", "model": "missing", "unbilled": True},
{"model": "measured", "calls": 1, "in": 10, "usd": 0.001}]
ledger.write_text("".join(json.dumps(row) + "\n" for row in rows))
got = receipts.read_codeaf_home(home, str(Path(home) / "stdout"))
self.assertEqual(got["calls"], 1)
self.assertEqual(got["models"], ["measured"])
self.assertEqual(got["cost_usd"], 0.001)

def test_marker_only_home_has_no_measured_calls_or_price(self):
with tempfile.TemporaryDirectory() as home:
ledger = Path(home) / "v3/usage.jsonl"
ledger.parent.mkdir()
ledger.write_text('{"kind":"unbilled-marker","model":"missing","unbilled":true}\n')
got = receipts.read_codeaf_home(home, "")
self.assertEqual(got["calls"], 0)
self.assertIsNone(got["cost_usd"])
self.assertIn("no measured calls", got["notes"][0])


if __name__ == "__main__":
unittest.main()
56 changes: 47 additions & 9 deletions cmd/codeaf/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,12 @@ type appAgent struct {
// job's effort when a send or Reopen opens it again
// ([appBackend.FactoryJobChats]).
jobEffort func() string
opening bool
// jobModel is the model of the factory job whose manager this is: the
// job's own, else its factory's default. A manager that has never been
// opened starts on it ([jobManagerOpenModels]); one that has keeps the
// pair saved on it.
jobModel func() string
opening bool
// openDone closes after initialization, including live publication and watchers.
// Waiters can abandon an open without leaving a goroutine on the gate.
openDone chan struct{}
Expand Down Expand Up @@ -1293,7 +1298,13 @@ func (b *appBackend) find(id string) (*appAgent, error) {
b.remember(row)
b.mu.Lock()
defer b.mu.Unlock()
return b.agents[id], nil
if a := b.agents[id]; a != nil {
return a, nil
}
// The folder answered under another id: an old-style chat with a moved
// copy ready is read back under its new id. No conversation has this one,
// and a nil agent with no error crashed every caller that used it.
return nil, appwire.Errorf(appwire.CodeNotFound, "there is no conversation with id %s", id)
}

// appRowOnDisk finds one conversation folder by id under the places root
Expand Down Expand Up @@ -1859,10 +1870,12 @@ func (b *appBackend) ensureLocked(a *appAgent) (appwire.Agent, bool, error) {
var view appwire.Agent
var transcript, workspace string
var boot *appBoot
var jobModel func() string
held := false
b.locked(func() {
view, held = a.view, a.live != nil
transcript, workspace = a.transcript, a.view.Workspace
jobModel = a.jobModel
if !held {
boot, a.boot = a.boot, nil
}
Expand Down Expand Up @@ -1902,6 +1915,9 @@ func (b *appBackend) ensureLocked(a *appAgent) (appwire.Agent, bool, error) {
ask.AutomaticModels = boot.automatic
ask.Models = boot.models
}
if ask.Models == nil && jobModel != nil {
ask.Models = jobManagerOpenModels(jobModel(), appDefaultFallback(config.ProfileDir()))
}
// OPENING IS SAID WHILE IT LASTS, so a caller that only reads or stops
// (agents.open, agents.stop, processes.list) does not queue on the gate
// behind it ([appBackend.findCrewOwner]).
Expand Down Expand Up @@ -2350,8 +2366,8 @@ func (b *appBackend) questionEvent(a *appAgent, id string, live *appLive, event
if scope != "" {
receipt.Text = "Always allowed · " + scope + " · you · " + at.Local().Format("15:04")
receipt.Link = &appwire.EntryLink{Label: "Settings › Permissions", Target: "settings:permissions"}
_ = b.keepAppEntry(a, receipt, q.Subject.CallID)
}
_ = b.keepAppEntry(a, receipt, q.Subject.CallID)
live.box.Event(appwire.EventEntryAdded, appwire.EntryAdded{Entry: receipt})
if moved {
live.box.Event(appwire.EventAgentChanged, appwire.AgentChanged{Agent: view})
Expand Down Expand Up @@ -2499,6 +2515,11 @@ func (b *appBackend) sendLocked(a *appAgent, text, clientID string, shown *appSe
if turn, handled, err := b.tryFactoryStatus(a, text, clientID, shown); handled {
return turn, err
}
if shown != nil && shown.creation != nil && shown.creation.Team == "" && !shown.queued && shown.from == "" {
if turn, handled, err := b.tryFactoryRecovery(a, text, clientID, shown); handled {
return turn, err
}
}
return b.startLocked(a, text, clientID, shown)
}

Expand Down Expand Up @@ -3025,7 +3046,7 @@ func (b *appBackend) pump(a *appAgent, id string, live *appLive, turn uint64, n
break
}
if words := strings.TrimSpace(event.Text); words != "" {
notice := appwire.Entry{ID: appLiveNoticeID(n, notices), Kind: appwire.EntryNotice, Text: redact.Secrets(words), Quiet: true}
notice := appwire.Entry{ID: appLiveNoticeID(n, notices), Agent: id, Turn: turn, At: b.now(), Kind: appwire.EntryNotice, Text: redact.Secrets(words), Quiet: true}
// ONLY WHAT THE PRODUCER TYPED travels as a fact; a notice
// with no kind stays ordinary and its words are never read.
switch event.NoticeKind {
Expand All @@ -3035,6 +3056,15 @@ func (b *appBackend) pump(a *appAgent, id string, live *appLive, turn uint64, n
notice.Skills = append([]string(nil), event.Skills...)
notice.Unrecorded = event.Unrecorded
entry(notice)
// AN UNRECORDED NOTICE IS IN NO RECORD A RETURNING SURFACE
// REDRAWS ([session.Event.Unrecorded]): memory's lines and a
// factory's progress are said for the moment or held in memory
// for the life of the session, never written anywhere a relaunch
// reads. The sidecar is that record, so only a recorded notice
// is kept; the live emit above still shows it now.
if !event.Unrecorded {
_ = b.keepAppEntry(a, notice, "")
}
notices++
}
case session.EventError:
Expand Down Expand Up @@ -3092,13 +3122,18 @@ func (b *appBackend) pump(a *appAgent, id string, live *appLive, turn uint64, n
if byPerson {
words = "Stopped by you · " + at.Local().Format("15:04")
}
entry(appFooter(appStopEntryID(n), id, turn, appwire.FooterStoppedByYou, words, at))
footer := appFooter(appStopEntryID(n), id, turn, appwire.FooterStoppedByYou, words, at)
_ = b.keepAppEntry(a, footer, "")
entry(footer)
case !ok && kind == appwire.ErrorProvider:
footer := appFooter(appErrorEntryID(n), id, turn, appwire.FooterProviderFailed, failure, b.now())
footer.ErrorKind, footer.Status = kind, status
_ = b.keepAppEntry(a, footer, "")
entry(footer)
case !ok:
entry(appwire.Entry{ID: appErrorEntryID(n), Kind: appwire.EntryError, Text: failure, ErrorKind: kind, Status: status})
errEntry := appwire.Entry{ID: appErrorEntryID(n), Agent: id, Turn: turn, At: b.now(), Kind: appwire.EntryError, Text: failure, ErrorKind: kind, Status: status}
_ = b.keepAppEntry(a, errEntry, "")
entry(errEntry)
}
if ok {
b.resetCloseAfterReply(id)
Expand Down Expand Up @@ -3131,9 +3166,12 @@ func (b *appBackend) pump(a *appAgent, id string, live *appLive, turn uint64, n
// person's messages (u0, u1 …), an answer is the m-th assistant message after
// it that said something (a<n>.<m>), and a tool entry is its call's own id
// (t<call>). The live pump counts as it streams and the listing counts as it
// reads, by one rule, so agents.entries names what entry.added named. What
// only ever exists live — a turn's error, an ask's receipt, the engine's dim
// notices — has an id of its own kind that the listing never makes.
// reads, by one rule, so agents.entries names what entry.added named.
// Entries the record itself never holds — a turn's error, an ask's receipt,
// the engine's recorded dim notices — take an id of their own kind, kept in
// the footer sidecar (appfooters.go) and laid back in by the listing. An
// unrecorded notice ([session.Event.Unrecorded]) keeps no id there either: it
// is said for the moment and gone on relaunch.

func appUserEntryID(n int) string { return "u" + strconv.Itoa(n) }

Expand Down
Loading