Files
magpie/internal
forever e961836fb0 usage, gateway, gui, provider, cli: the tests pass when a run crosses midnight, and a usage answer reads the clock once (#1046)
* usage: the usage tests pass at any time of day, midnight included

The usage tests stamped their calls with the machine's time and then
asked for today, 7 or 30 days, or a cached answer, which the code
worked out from its own time.Now a moment later. A minute after
midnight a call made "a minute ago" was yesterday's, and when midnight
fell between the test's read and the code's, the call and the period
were of two days. magpie usage, the TUI's Usage page, the two Codex
cache-write tests and the computers' shared days failed in the first
minutes after midnight (the TUI's after midnight UTC, the zone its
package's tests run in since 1caea638), and 28 more failed when
midnight fell inside them.

usage.Clock is now the time every period is read by: the start of
today, 7 days and 30 days (LedgerOf, Summarize, Direct, QueryPage), the
hours or days of the chart (LedgerSeries, QueryPage), the day a cached
summary or page is for, the time Append gives a call that has none,
the days davsync shares and the date in an exported CSV's name. It is
time.Now, as before, and a variable for the tests, like LogCalls. Each
test that built its calls from the time now holds Clock at noon on
2026-09-30 (holdClock; holdUsageClock in gui and the CLI; three lines
in tui and davsync) and stamps its calls from that instant, among them
TestReasoningReplyTimedByItsAnswer, whose timeline rows 117b6c7f made
at time.Now. TestPeriodsReadTheClock checks that every one of these
reads goes through Clock. TestQueryPageSourceAndIdentityInvalidation is
held too; that it fails when run alone is fixed apart from this, in
#1045.

Seven held tests could not fail at midnight before, and are held so
that the tests read one clock: TestUsageSameSizeAndMtimeRewrite (All's
totals), TestDirectCountsOnlySessionFileCalls (30 days around its own
noon), TestQueryPageIOOutsideCacheLock (a file two days old), gui's
TestUsageLedgerDayRoutes (7 days hold its calls from either day),
TestLedgerSingleSnapshotAndLocalSession and
TestLedgerNamesModelAtProvider (their calls are stamped after the code
reads the time), and TestUsageRawAndPricedChunksShareBudget, which
fails only when the midnight in it starts a new week of All's chart, on
a Sunday night; at.sh sets the time of day and not the weekday, so that
one was not shown.

Verified with at.sh, a scratch script that runs a test binary under a
zone made with zic, so that the local time of day is the one asked for
as the run starts (HOME in a sandbox, Go's caches pinned). The runs were
first made on c5a54bc7, and those marked (698da36f) again on 698da36f,
with the same results. The tui package runs in UTC since 1caea638, a
zone at.sh doesn't move, so its runs on 698da36f used copies of both
trees whose TestMain keeps the zone TZ sets.

Before, at 00:00:30, 00:01:30 and 12:00:00 (698da36f):
  TestUsageShowsCallsNotThroughMagpie, 00:00:30:
    usage_cli_test.go:95: no calls today · route an agent through
    magpie and its usage shows up here
  00:01:30: "5 tokens today · 1 call · no price", without the calls
  not through magpie; TestUsagePageShowsCallsNotThroughMagpie (tui)
  fails the same two ways at usage_test.go:138
  --- FAIL: TestCodexCacheWriteNotInInput (00:00:30)
    codex_cache_write_test.go:104: rows []
  --- FAIL: TestCodexCacheWriteMatchesGatewayCall (00:01:30)
    codex_cache_write_test.go:73: filter {...}: 0 rows, 0 calls, want
    the gateway's 2: []
  --- FAIL: TestUsageSharedBetweenComputers (00:01:30)
    usage_share_test.go:134: a's days on the server:
    [...-2026-10-05 ...-2026-10-06 ...-2026-10-07]
  12:00:00: PASS

Midnight inside a test, in scratch copies of the head. Started at
23:59:59.3 with the code's first period read held 1.5s, so that it
reads after midnight what the test stamped before it, 16 usage tests,
3 gui tests and TestUsageCSV FAIL, among them
  ledger_test.go:257: today: 0
  review_test.go:69: chart counted 0
  incremental_log_test.go:202: concurrent append lost
  TestCachedSummaryRemainsCallerOwned: panic: runtime error: index out
  of range [0] with length 0
  usage_test.go:121: second page: {... Period:today Rows:[] ...
  Total:0 ...}
  usage_test.go:311: "": magpie-requests-7d-2026-10-06
Started at 23:59:55 with the test waiting past midnight between two of
its reads, 7 more FAIL:
  request_day_test.go:45: selecting a day changed the chart or its
  legend
  cache_bounds_test.go:84: uncached today summary stale
  growing_rewrite_test.go:71: today summary stale
  cache_bounds_test.go:153: page key changed instead of exercising the
  same cache
  priced_blocks_test.go:55, caller_provider_test.go:129 and :172: page
  differs
TestReasoningReplyTimedByItsAnswer, with a scratch 1.5s sleep before
its LedgerSeries, started at 23:59:59.3 (698da36f):
  usage_test.go:255: part: {Calls:0 Tokens:0 Cost:0 Timed:0 TTFT:0
  DecodeMs:0 DecodeOut:0}

After: the same at.sh runs PASS at 00:00:30, 00:01:30 and 12:00:00
(698da36f); the same scratch copies made of this change PASS, each
test taking 1.5s or 5.2s, so midnight did fall inside it (698da36f for
TestReasoningReplyTimedByItsAnswer's); and every changed test PASS at
23:59:59.9, 00:00:30 and 12:00:00 (698da36f).

Without the change (698da36f): with the reads put back on time.Now, 21
usage tests, 6 gui, 2 CLI, the tui and the davsync test fail, among
them
  --- FAIL: TestPeriodsReadTheClock
    clock_test.go:55: Direct(Today): 0 calls since 2026-10-06 00:00:00
    -0700 PDT, want 1 since 2026-09-30 00:00:00 -0700 PDT
    clock_test.go:63: Summarize(Today) the next day: 1 calls, want 0
  --- FAIL: TestReasoningReplyTimedByItsAnswer
    usage_test.go:255: part: {Calls:0 Tokens:0 ...}
Each of the eleven reads put back alone fails a test:
TestPeriodsReadTheClock for the nine in usage (LedgerSeries:
"clock_test.go:52: LedgerSeries(Today): 0 calls, want 2"; the page's
cache key: "clock_test.go:66: QueryPage(Today) the next day: 1 rows,
want 0"), TestCSVStamp for the CSV's name (usage_test.go:325: "":
magpie-requests-7d-2026-10-06) and TestUsageSharedBetweenComputers for
davsync (usage_share_test.go:224: all computers: 4 rows, 707 in; want
5, 737).

On this commit: go test -tags nogui -count=1 . ./internal/usage
./internal/tui ./internal/davsync ./internal/gui ok; -race -count=20
over the changed tests of each of these packages ok; gofmt -l prints
nothing, and go vet -tags nogui on the five packages finds nothing.

* usage: pages, CSVs and magpie usage asked at midnight show one day

An answer about usage read the time more than once. The Requests page
read it for the day it is kept for, for the period its log and session
files are read over, for the period of its rows and for its chart's
hours; the page from the ledger (QueryPage's path when LogCalls is
set, as in the tests) for its rows and for its chart; Direct for its
period and for its rows; a CSV for its rows and for its name;
magpie usage and the TUI's Usage page for Summarize and then for
Direct. Most of these reads have the log and the session files read
between them, seconds on a large log, and when midnight fell between
two of them the answer was of two days: yesterday's calls listed over
today's empty chart, Direct starting yesterday and counting none of
yesterday's calls, the calls not through magpie left out of magpie
usage and the Usage page (they were the new day's, none yet), and
yesterday's calls saved as magpie-requests-today-<the new day>.csv.

Each answer now reads Clock once and passes that moment down. queryPage
reads it for its key, its sources, its rows and its chart
(buildRequestBlocks takes it); the page from the ledger reads its rows
and its chart at it (LedgerOfAt, ledgerSeriesAt); Direct reads the
ledger at the moment of its period. usage.Summaries(p) is Summarize(p)
and Direct(p) at one moment, and magpie usage and the TUI's Usage page,
when a period is picked and when it is read again, ask for it: one
exported name, the clock left inside usage and one line changed at
each caller, where SummarizeAt and DirectAt would be two names and
each caller would read the clock itself. The CSV download and export
read their rows with the new usage.LedgerOfAt at the moment their file
is named for, which csvStamp now takes; the rows and the name are read
in two packages, so LedgerOfAt is exported. LedgerOf, LedgerSeries,
Summarize, Direct and QueryPage keep their signatures, each reading
Clock once.

The new tests set Clock to read a tenth of a second before midnight
the first time and a tenth of a second after it from then on, set
again for each answer. The midnight is the one that starts 2026-10-01,
local time: a past instant, at midnight because that is what is
tested, and the day before it is the 2026-09-30 the held tests use.
TestAnswerReadsTheClockOnce asks QueryPage(Today) from the indexed log
and from the ledger: its 3 rows, one from a session file written that
day at 23:00, are under that day's chart, and asked again after
midnight the page is the next day's, not the one kept for the day
before; Direct(Today) counts the call and starts that day.
TestUsageAskedAtMidnight (magpie usage today), TestUsagePageAtMidnight
(the TUI's Usage page, today picked and read again) and
TestUsageCSVAtMidnight (download and export) check that the calls
through magpie and those not through it, or the rows and the file's
name, are of the day asked on. TestCSVStamp passes its time.

Verified on the previous commit, at.sh as there. The runs were first
made on c5a54bc7, when the tui package's tests still ran in the local
zone, and those marked (698da36f) again on 698da36f, with the same
results.

Before: the new tests on the previous commit, at any time of day
(at.sh at 12:00:00 too) (698da36f):
  --- FAIL: TestAnswerReadsTheClockOnce
    clock_test.go:131: QueryPage(Today): 0 rows, 0 in a chart from
    2026-10-01 00:00:00 -0700 PDT; want 3 under one from 2026-09-30
    00:00:00 -0700 PDT
    clock_test.go:141: QueryPage(Today): 3 rows, 0 in a chart from
    2026-10-01 ...; want 3 under one from 2026-09-30 ...
    clock_test.go:145: Direct(Today): 0 calls since 2026-09-30
    00:00:00 -0700 PDT, want 1 since 2026-09-30 00:00:00 -0700 PDT
  --- FAIL: TestUsageAskedAtMidnight
    usage_cli_test.go:154: 5 tokens today · 1 call · no price ...,
    with no part for the calls not through magpie
  --- FAIL: TestUsagePageAtMidnight
    usage_test.go:184: today picked: ... (the same)
    usage_test.go:187: read again: ...
  --- FAIL: TestUsageCSVAtMidnight
    usage_test.go:367: download: 200, attachment;
    filename="magpie-requests-today-2026-10-01.csv" with 2 rows
    (<nil>), want magpie-requests-today-2026-09-30.csv with 2
    usage_test.go:378: export: 200
    {"path":"~/Downloads/magpie-requests-today-2026-10-01.csv",
    "rows":1}, want magpie-requests-today-2026-09-30.csv with 1 row

With the real clock, in scratch copies of both trees: scratch tests
held one of Clock's reads 1.5s after it was made, as reading a large
log does (the first; for one of them the page's third, its chart's),
and at.sh started each at 23:59:59.3. Before, all 9 FAIL, among them
  asked 23:59:59.564, answered 00:00:01.068
  Direct(Today): 0 calls since 2026-10-05 00:00:00 -0940 MID, want 1
  since 2026-10-05 00:00:00 -0940 MID
  QueryPage(Today): 2 rows, 0 in a chart from 2026-10-06 ...; want 2
  under one from 2026-10-05 ...
  download: attachment; filename="magpie-requests-today-2026-10-06.csv"
  with a call of 2026-10-05 23:59
After, all 9 PASS, answered at 00:00:00.9 to 00:00:01.0, so midnight
fell inside each, except the chart's: its held read is gone with the
change. A scratch sleep before the chart shows that one: before "2
rows, 0 in a chart from 2026-10-06", after PASS, asked 23:59:59.344
and answered 00:00:00.941. At 12:00:00 all of them PASS in both trees.

After: every changed test PASS with at.sh at 23:59:59.9, 00:00:30 and
12:00:00 (698da36f).

Without the change (698da36f): with the production put back to the
previous commit's (gui/usage.go only so far that TestCSVStamp compiles:
csvStamp takes the time, read apart from the rows as before), go test
-count=1 . ./internal/usage ./internal/tui ./internal/gui fails the four
new tests and no other, with the lines above. Each of the 16 places put
back alone fails one of them: the page's key (clock_test.go:134: "3
rows, 3 in a chart from 2026-09-30 ...; want 0 under one from 2026-10-01
..."), its sources (:131: "2 rows"), its rows (:131: "0 rows"), its
chart (:131), the ledger page's rows and chart (:141), LedgerOfAt (:141,
:145), ledgerSeriesAt (:141), Direct (:145); Summaries, indexedSummary
and magpie usage (usage_cli_test.go:154); Summaries and the TUI's period
key and reload (usage_test.go:184, :187); the CSV's download and export
(usage_test.go:367, :378).

On this commit: go test -tags nogui -count=1 . ./internal/usage
./internal/gui ./internal/tui ./internal/davsync ./internal/gateway ok;
-race -count=20 over the changed tests of each package ok; gofmt -l
prints nothing, and go vet -tags nogui finds nothing.

* gateway: the key-limit tests pass when a run crosses local midnight

A go test run that crossed local midnight could fail the gateway's
key-limit tests. keyLimited and the /v1/magpie/limit endpoint each read
time.Now() when they check a key, and access.Window turns that into a
calendar window (a day from midnight, a week from Monday, a month from
the 1st). The tests seed "yesterday" from their own time.Now() and then
make several calls, so a run that crossed midnight saw its calls split
over two windows: the second call of a 350-token day was let in, or a
restart counted nothing.

The gateway's key-limit path now reads limitClock, a variable for the
tests like ruleClock and idClock, and production behaviour is unchanged
(it is time.Now). TestKeyLimitRefusesBeforeUpstream,
TestKeyLimitHoldsParallelRequests and TestKeyLimitWindowResets pin it
at 2026-09-30 12:00 local and seed "yesterday" from that instant. Calls
are still recorded at the real time, after the pinned one, and a window
counts every call from its start, so they all count in the pinned
window; the "tomorrow's window" check in TestKeyLimitWindowResets stays
on the real now, the first window with none of those calls in it. No
other test sets a key limit with records in its window (the gateway-key
CLI test and the GUI caller-key tests count 0 calls or have no limit).

TestKeyLimitRefusesBeforeUpstream now also checks that the 429's
Retry-After and X-Magpie-Limit-Reset name the next midnight after the
check, and that the limit endpoint's week starts on the pinned
Monday.

Verified with at.sh, a scratch script that runs the test binary under
a zone made with zic, so that the local time of day is the one asked
for as the run starts:
- before, at.sh at 23:59:59 with COUNT=500/300 and FRAC 0.3/0.6/0.8:
  3 of 6 runs of each test failed with
  key_limit_test.go:217: spent key let in
  key_limit_test.go:117: a restart forgot what the key used 200
  key_limit_test.go:121: status &{Period:day Start:2026-10-06 ...
  Calls:0 Tokens:0 ...}
- after, the same 6 runs (COUNT=500, both tests) print PASS, and so do
  ^TestKeyLimit at 12:00:00 and 00:00:00 with COUNT=20.
- go test -tags nogui -count=1 ./internal/gateway: ok (79.5s);
  -race -count=20 -run '^TestKeyLimit' ./internal/gateway: ok.
- gofmt -l prints nothing; go vet -tags nogui ./internal/gateway is
  clean.

The month boundary (TestKeyLimitHoldsParallelRequests) and Monday can't
be reproduced by changing the time of day; the pinned clock covers them
the same way.

Fails without the change (tests pinned, keyLimited back on time.Now):
--- FAIL: TestKeyLimitRefusesBeforeUpstream
    key_limit_test.go:107: headers map[... Retry-After:[80228]
    X-Magpie-Limit-Reset:[2026-10-07T00:00:00-07:00] ...]
and with only the limit endpoint back on time.Now:
--- FAIL: TestKeyLimitRefusesBeforeUpstream
    key_limit_test.go:159: limit endpoint {... "period":"week",
    "start":"2026-10-05T00:00:00-07:00" ...}

* gateway: the routing history tests pass when a run crosses midnight

TestHistoryKeepsDaysAndDropsOld and TestRemoteMagpieGroupHistory took
now := time.Now() and saved routes at now and now+1s. saveRoute files a
route by its own day (r.Time.Local()), so a test started at 23:59:59 put
the +1s route in tomorrow's file and History(today) found one route
less. TestHistoryKeepsSessionAndTokenTiers raced the other clock:
saveRoute prunes by time.Now(), so when the day turned between the
test's time.Now() and the first saveRoute, the prune gzipped "today"
after the first route, the second went to a new .jsonl, and the test's
own pruneHistory gzipped that over the first .gz, losing route 1.

saveRoute now prunes by historyClock (time.Now outside the tests). The
three tests take historyNoon(t), which holds that clock at
2026-09-30 12:00 local and returns it as their now, restored with
t.Cleanup. Every route, day file and prune is then on that one past day.
The fixture can't be a fixed past day without the clock: pruning by
the real today gzips that day's file at once (and, a month on, drops
it as too old).

The other history tests keep the real clock:
TestHistoryRouteOutsideDayLimit writes its day files itself from one
now and the code under test reads no clock, and
TestHistoryRouteDayAndNoLock already uses a fixed day.

Verified with at.sh, a scratch script that sets the local time of day
through a zone made with zic as the run starts:
- before, at.sh ./internal/gateway at 23:59:59:
  --- FAIL: TestHistoryKeepsDaysAndDropsOld
      history_test.go:50: days [{2026-10-06 1} {2026-10-05 1} {2026-10-04 1}]
  --- FAIL: TestRemoteMagpieGroupHistory
      remote_group_served_test.go:144: routes [... only ID 1 ...]
  and, with a 2s sleep after now := time.Now() standing in for the turn
  of the day, --- FAIL: TestHistoryKeepsSessionAndTokenTiers
      history_test.go:22: compressed history lost accounting: [only ID 2]
  12:00:00 control: PASS.
- after: the three tests PASS at 23:59:59, 23:59:59.9 (FRAC=0.9),
  12:00:00, and 23:59:59 with the same 2s sleep; all TestHistory* and
  TestRemoteMagpieGroupHistory PASS at 23:59:59 with -test.count=3.
- with saveRoute pruning by time.Now() again (the fixtures still pinned),
  at any time of day:
  --- FAIL: TestHistoryKeepsSessionAndTokenTiers
      history_test.go:32: compressed history lost accounting: [only ID 2]
- go test -tags nogui -count=1 ./internal/gateway: ok;
  -race -count=20 on the three tests: ok; gofmt -l and
  go vet -tags nogui ./internal/gateway print nothing.

* gateway, davsync: the request archive tests pass when a run crosses UTC midnight

The archive names a call "<date>/<id>" from the call's own start, in
UTC (archiving in archive.go). TestArchiveOn read time.Now().UTC()
before posting and TestRequestArchiveToS3 read it before post(), and
each expected that read's date. A run where UTC midnight (17:00 PDT)
fell between the test's read and the request's start expected the day
before the one the archive used, and failed.

Both tests now take the date from the Time the call carries
(s.Recent()[0].Time, the same start the archive's name is made from),
in UTC, so nothing is read from the clock a second time. No production
code changes. The other archive tests already go by the call's Archive
name, and internal/gui's archive test uses a fixed name; no other test
in internal/gateway, internal/davsync or internal/gui predicts an
archive key or a UTC-dated name from its own clock.

Verified: at.sh only moves the local time of day, and this is a UTC
crossing (UTC midnight was ~16h away), so it was shown in a scratch
copy whose serve starts each request 0.2s after the next UTC midnight,
as a request does when the test read the clock just before it. There
the old tests fail:
  archive_test.go:115: call archive "2026-10-07/000000-f4690de23ee8a859",
  want "2026-10-06/000000-f4690de23ee8a859"
  archive_test.go:93: nothing at
  "team x+y/magpie/archive/2026-10-06/000000-0e74a87816ef6a56.json"
and the new ones pass. Unshifted, under at.sh at local 23:59:59,
00:00:00 and 12:00:00, both pass, as the old ones do at 23:59:59 and
00:00:00 (local time doesn't enter the name). With archiving naming by
start.Format (local) instead of start.UTC().Format, both new tests fail
under at.sh at local 23:00:00, where the local date is a day behind
UTC's:
  --- FAIL: TestArchiveOn ... archive_test.go:116: call archive
  "2026-10-05/082500-7322712bd89f71da", want
  "2026-10-06/082500-7322712bd89f71da"
  --- FAIL: TestRequestArchiveToS3 ... archive_test.go:95: nothing at
  "team x+y/magpie/archive/2026-10-06/082500-3587ab4192da2cd5.json"
and pass at 12:00:00. go test -tags nogui -count=1 ./internal/gateway
./internal/davsync passes in a sandbox HOME; -race -count=20 -run
'^(TestArchiveOn|TestRequestArchiveToS3)$' passes; gofmt -l and go vet
-tags nogui on both packages are clean.

* gui: TestWhatsNewTodayRoute passes when the run crosses local midnight

TestWhatsNewTodayRoute posts {"on":true} to /api/whatsnew/today, which
writes today() to whatsnew-quiet-day, then checks quietToday(), which
compares the file with a second today(). A run that crossed local
midnight between the two read different days and failed with
"on: 204 false". The neighbouring TestWhatsNewQuietAfterRestartToUpdate
already pins today; this test didn't.

The test now pins today to 2026-09-30 (restored with t.Cleanup), so the
post and the check read the same day. The other What's new tests start
from an empty folder, so quietToday finds no file and never reads
today().

Verified: with a temporary 1s sleep between the post and the check, the
unpinned test under at.sh (local time of day set by a zone) started at
local 23:59:59.5 failed with "whatsnew_test.go:237: on: 204 false" and
passed at 12:00:00; pinned, it passes at 23:59:59.5 (same sleep), and,
without the sleep, at 23:59:59, 00:00:00 and 12:00:00. With
setQuietToday writing time.Now().Format("2006-01-02") instead of
today(), the test fails: "--- FAIL: TestWhatsNewTodayRoute ...
whatsnew_test.go:239: on: 204 false". go test -tags nogui -count=1
./internal/gui passes in a sandbox HOME; -race -count=20 -run
'^TestWhatsNewTodayRoute$' passes; gofmt -l and go vet -tags nogui
./internal/gui are clean.

* provider: TestZCodeStartSentAsTheApp passes when the run crosses midnight

A ZCode Start Plan request opens with a context prefix that says
"Today's date is <day>" (zcodeDateReminder), the day zcodeStartRequest
took from its own time.Now(). TestZCodeStartSentAsTheApp sent the
request, then built the expected prefix from a second time.Now(). A run
that crossed midnight between the two failed on "the context prefix",
the request holding the day before.

zcodeStartRequest now takes the time from zcodeClock (a package
variable, time.Now outside tests, as gateway's ruleClock), and the test
pins it to 2026-09-30 12:00 local, restored with t.Cleanup, and expects
"Today's date is 2026-09-30." The test sets TZ=Asia/Shanghai, which
moves time.Local only when nothing has read it yet; a mid-day instant in
time.Local formats to the same day either way. The other calls to
zcodeStartBody in the test check shaping only, not the day.

No other provider test builds a day string from a second clock: the
WorkBuddy credits, daily warm-up and check-in tests pass fixed times to
code that takes now as an argument, and TestQoderCheckinOnTheCard's day
is only carried, not compared.

Verified with at.sh (local time of day set by a zone, run as
-run '^(TestDailyCredits|TestZCodeStartSentAsTheApp)$' so that
time.Local is read before the test's t.Setenv("TZ"), as in a full run;
alone, Local becomes Asia/Shanghai): with a temporary 1s sleep after the
request, the unpinned test started at local 23:59:59.5 failed with
"zcode_client_test.go:195: the context prefix: ... Today's date is
2026-10-05." and passed at 12:00:00; pinned, it passes at 23:59:59.5
(same sleep) and, without the sleep, at 23:59:59, 00:00:00 and 12:00:00.
With zcodeStartRequest back on time.Now(), the pinned test fails:
"--- FAIL: TestZCodeStartSentAsTheApp ... zcode_client_test.go:196: the
context prefix: ... Today's date is 2026-10-06." go test -tags nogui
-count=1 ./internal/provider passes in a sandbox HOME; -race -count=20
-run '^TestZCodeStartSentAsTheApp$' passes; gofmt -l and go vet -tags
nogui ./internal/provider are clean.

* accounts, quota: a window's reset reads against one time per listing; TestQuotaCell passes across midnight

quotaCell, a window in `magpie accounts` and `magpie quota`, read the
clock three times: whether the reset had passed, how long until it, and
ResetClock's label, which names the calendar day ("tomorrow 02:13" or
"02:13"). TestQuotaCell set the reset 2h13m30s from time.Now() and
built the expected label from yet another time.Now(); a run that crossed
local midnight between quotaCell's read and the test's got "tomorrow
02:13" from one and "02:13" from the other and failed.

quotaCell now takes now. accountsCmd and quotaCmd take one time.Now()
just before printing and pass it to every window's cell, so a listing's
windows agree on what "today" is. The test passes a fixed 2026-09-30
12:00 local and expects "↻2h13m 14:13", and for a reset the next day
"↻14h13m tomorrow 02:13"; no zone in tzdata 2026c changes its clock
between those instants, so both read the same in any TZ. resetsCell's
label, quotaReadingCell's expired check and quotaResetCmd's prompt
still read time.Now(); no test compares them against a second clock.
internal/tui's quotaCell and quotaLines already take now, and their
tests pass a fixed one.

Verified with at.sh (local time of day set by a zone): with a temporary
1s sleep between quotaCell and the check, the old test started at local
23:59:59.5 failed with "accounts_cli_test.go:128: \"5h 42% ↻2h13m
tomorrow 02:13\"" and passed at 12:00:00; the new one passes at
23:59:59.5 (same sleep) and, without the sleep, at 23:59:59, 00:00:00
and 12:00:00. Putting any of quotaCell's three reads back on the wall
clock fails the test ("--- FAIL: TestQuotaCell"). ResetClock's now
alone fails the next-day case: "accounts_cli_test.go:134: tomorrow:
\"7d 42% ↻14h13m 02:13\"". The passed check alone, or all three:
"accounts_cli_test.go:129: \"5h 42% · reset time passed Sep 30
14:13\"". untilShort alone: "accounts_cli_test.go:129: \"5h 42% ↻now
14:13\"". go test -tags nogui -count=1 . passes in a sandbox HOME;
-race -count=20 -run '^TestQuotaCell$' passes; gofmt -l and go vet
-tags nogui . are clean.
2026-10-07 02:14:29 +08:00
..
gateway: a gateway key can be held to some accounts (#905, LCorleone) — a key could burn any account a provider had: two coding plans of one vendor, and a key given to someone had them both. A key now keeps an optional list of accounts and keys (caller-keys.json "accounts", none = every account, so older files read as before): "<provider>/<account>", an account by its stable id — the login's, set once it is written and kept through renames (logins.json's savedLogin id, written by every write and stood in for by one made of the name until then, so an entry made before stands), a key by its fingerprint — every one a provider has when the list is set, and one gone later (a key rotated, an account signed out) kept as it is, matching nothing, holding the key closer, never wider. The command line and the menu name an account by who is signed in and keep its id. Routing holds a plan to the key's accounts beside its models: a candidate whose account the key may not use is left out — through a routing group the key names no less, for naming a group is not naming its accounts — the plan's order keeping step and its left-out list marking what the key held out (Weighed's held); magpie's own calls for the key — a web search, a picture described, a Codex title — are of the models the user picked, so the models don't hold them, but their spend lands on an account all the same, and the accounts hold them too. When every account or key a model has is one the key may not use, the request is refused with a 403 in the API's own error shape naming the model and the accounts the key may use, logged as a rejected call: after a usage cap's 429 and before an account barred for the model's, itself before the no-member 404. /v1/models and the Anthropic and Gemini lists show the key only the models an account or key it may use serves (a provider's account set not to serve one, #474, is none of the key's); a provider with no account and no key (a local ollama) is one the key names no account of — its models stay listed and its asks pass — while an entry kept after its provider lost its last account or key serves none, holding the key closer. The embeddings, images and videos endpoints hold their candidate to the key's accounts the same way, and counting tokens counts on those alone (the local estimate where it may use none). System One asks its decision model of an account or key the key may use — and sends on that key, of those in use, not the provider's first, which a key held to a later one was spent on; the videos and images keyed paths send on one the key may use too, held to a later key no longer refused for the first alone. Set it in the app's menu the models are picked in — the accounts after the models, an account by who is signed in with their plan in the note, a key by its fingerprint, what the CLI kept that no provider has now kept as "Not signed in now" — or magpie gateway-key accounts <id> codex/me@example.com openai/<key id> (all takes it off); gateway-key list has an ACCOUNTS column. en/zh/ja/de, the German "{n} accounts" main already had left to it rather than written twice. The images API's fan-out over a subscription's accounts is held to the key's accounts as one: one of the key's whose plan refuses to draw no longer hands the pen to an account it may not use, and the second in the order, when it is the key's, draws what the first may not (drawOnAccounts filters the candidates, the account pinned to with the account header told plainly when the key may not use it). The Routing view's left-out list says an account the key held out for what it is, not that its plan lacks the model, in Chinese, Japanese and German too; the key's badge names its accounts by who is signed in, not by the stable ids it keeps. (#932)
2026-10-06 10:51:50 +08:00
appdir: a folder named data in /Applications isn't taken for magpie's portable data while ~/.config/magpie has its files (#989, Treyang: after updating 0.1.1077 → 0.1.1078 magpie opened with no providers, sign-ins or settings; ~/.config/magpie was intact and /Applications/data had appeared) — portable mode (#508) took any folder named data beside the folder holding Magpie.app, and on a Mac that folder is /Applications, which every app shares. The reporter's /Applications/data began with nodes/, providers/, tools/, skills/ and an initialized marker, none of which magpie makes (magpie keeps providers.json, not a providers folder); magpie's cli-identity.json, install-id and last-version came after, written once a magpie had taken the folder for its own. The updater didn't make it: Stage only unpacks and codesign-checks the new app in .magpie-update, Install renames, Relaunch opens the bundle, and nothing runs the staged copy; appdir never creates a data folder without a .portable marker either. So a folder another app left there turned an installed magpie portable and empty at its next start. Now, in /Applications or ~/Applications, a data folder is magpie's portable data only while the installed folder (~/.config/magpie, or $XDG_CONFIG_HOME/magpie) holds no providers.json, logins.json or settings.json — one who keeps magpie portable there never ran it installed, and keeps it — or with a .portable marker beside the app, which makes it portable as before. A data folder beside magpie anywhere else (a USB stick, a Windows or Linux folder of its own, an AppImage's) is taken as before. A user already hit is back on ~/.config/magpie at the next start, and the data folder is left as it is, never deleted or moved; the log says it was passed over. TestResolveSharedFolder (/Applications stood in by a temp folder: a data folder with nodes/ and install-id, installed state present/absent, .portable, a data folder on a folder of its own) fails without the change (Resolve and Config give the shared data folder) and passes; TestSharedFolderIsApplications pins which folders count. go test -race ./internal/appdir/ (-count=3), go test ./internal/settings/ ./internal/shortcut/ ./internal/update/, go vet (darwin, windows), darwin/linux(nogui)/windows builds pass. Not tried with the reporter's /Applications or the updater on a real install.
2026-10-06 14:48:39 +08:00
settings: keep awake can keep the display on too (#975, Hu9956: magpie's keep-awake held the system up, but the screen still went black and locked, and their agents record the screen to check their work, which a locked screen stops) — Settings › General's "Keep awake while agents work" row now has three segments, Off / On / Screen on too, saved as keepAwake and the new keepAwakeDisplay (default off, so On and every existing settings.json behave as before). awake.Keep takes a Level (Off, System, Display) in place of a bool; a level changed while held releases and takes the hold anew. On a Mac the display level runs caffeinate -i -d -w <pid> (pmset -g assertions on a caffeinate of the same flags shows PreventUserIdleDisplaySleep beside PreventUserIdleSystemSleep); on Windows SetThreadExecutionState gets ES_DISPLAY_REQUIRED too; on Linux logind's idle inhibitor (systemd-inhibit --what=idle:sleep, unchanged) is all there is, and whether the screen stays on is up to the desktop. keepAwakeDisplay is this computer's own (KeepOwn) and goes in prefsKeep, so another setting's save keeps it. The row's line says the display stays on when it's chosen; it keeps its English in data-en, so a language change re-translates it. zh/ja/de strings added. No TUI or CLI has keep-awake, so none changed. TestKeepDisplay (taken once, taken anew with the display, not again at the same level, back to System, released at Off), TestCaffeinateArgs, TestExecutionState (GOOS=windows test build) and TestKeepAwakeKeptAndOwn cover it; go test ./internal/awake ./internal/settings ./internal/gui ./internal/gateway pass, -race -count=5 on awake. keep-awake.test.cjs now runs en/zh/ja/de at 900px and 440px in Chromium and WebKit (display posts keepAwake+keepAwakeDisplay true, the line changes, another setting's save keeps it, segments fit the row, On and Off clear it, no click moves the page): 32/32 twice; gui-ja, gui-de, i18n-keys and click-scroll pass. go vet (darwin, windows, linux), darwin, linux (nogui) and windows builds pass. Not tried with an agent recording the screen; the reporter is asked to confirm.
2026-10-06 15:33:28 +08:00
catalog: a model the provider's catalog doesn't list is named as the other providers name it (John on Discord #feedback: GLM Coding Plan and ZCode name the same models in different case, in the provider list and the group picker) — the Coding Plan's /models (open.bigmodel.cn and api.z.ai, read with a real key) gives ids alone, glm-5-turbo, glm-5.3-flashx …; catalog.Decorate names only those Zhipu's models.dev entry (zhipuai) lists, so glm-5-turbo kept its id as its name beside ZCode's GLM-5-Turbo. models.dev's load now keeps each model's name as most providers serving it give it (a name that is only the id casts no vote), catalog.NameOf reads it by bare id in any case, and catalog.Named fills it in for a model still named by its id: in Available's live list and for a pick the list doesn't have. Ids, and what is sent upstream, are unchanged; a name the user gave stays theirs (Catalog's Name, with the new one as Default); an Azure provider is left out, its deployments named as their owner named them. TestModelNamedAsElsewhere (the Coding Plan's real ids, a user name, an Azure deployment) fails without the provider change and passes; TestNameOf is new. TestSearcherChosen read whatever catalog an earlier test had left loaded, and now resets it: with none, its lists' models go by their ids, as it expects. go test ./internal/catalog/ ./internal/provider/ ./internal/gateway/ ./internal/gui/ . pass; -race -count=5 for the two new tests; go vet, linux(nogui)/windows builds pass. No GUI change (the GUI and the group picker show name || id).
2026-10-06 13:09:40 +08:00
windows: an account list read while another magpie writes it is waited for, not taken for no accounts (Packing1 on Discord, Windows 11: 早上更新重启后之前登录的Antigravity订阅供应商和账号都没了 … 整行都没了,供应商里面antigravity直接消失了) — on Windows a file being renamed over (every write magpie makes: a temp file renamed onto it) is, for a moment of up to ~2ms, "The system cannot find the file specified" to anyone else looking at it, with MoveFileEx and with POSIX-semantics renames alike. filememo.Read gave that back as an error, readLogins as no accounts, and whatever change came next (any account's read-modify-write) wrote the empty list back: every built-in account gone, its subscription with it. Measured on Windows 11 (10.0.0.132): one process replacing a 200 KB file, another stat-ing and reading it, saw the file "not there" in a few windows each 20 s. Now steady.Stat and steady.ReadThere wait out a moment of a file not there (15 tries a millisecond apart, Windows only), and filememo looks through them, remembering a file found not there so one that really isn't costs the wait once. A magpie started by an update now waits for the one it replaces before its start-up moves write any file (AwaitPredecessor at the top of run, not only in the GUI). TestLoginsKeptWhileAnotherMagpieWrites (Windows: two writers replacing logins.json, readLogins never without the Antigravity account) failed 1 in 3 and 2 in 8 runs on the old code ("read as no accounts 8 times"), passed 11 of 11 on this; filememo's TestReadWhileReplaced the same (1180 misses on the old code); TestStatWaitsOutAReplace everywhere. A real update in a sandbox home on the Windows box, magpie-cli 0.1.880 → 0.1.942 from the release, kept the account and logins.json byte for byte. go vet (darwin, and windows nogui), builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 10:57:45 +08:00
library, imagemcp: magpie's own image server is given to agents in WSL, started through WSL's interop with the paths crossing over (#900, cjyrainbow) — on Windows the Library names magpie-image by this machine's binary ('D:\tools\Magpie\magpie-windows-amd64.exe' mcp image), and turning it on for "Codex · WSL Ubuntu" or "Claude Code · WSL Ubuntu" failed: a Windows program was refused for every agent in WSL. WSL's interop starts a Windows .exe from inside the distro, so magpie's own server (args mcp image, whatever its binary) is now written for an agent in WSL by the binary's path in the distro, asked of the distro itself (wslpath -u, so a custom automount root is followed; asked once in ten minutes; /mnt/<drive>/ when it can't be asked), with --wsl <distro>, --mount where the drives aren't under /mnt/, --home the distro's $HOME, and WSLENV naming the variables it is given (MAGPIE_ADDR), which a Windows program otherwise doesn't get. A distro whose interop is off (no WSLInterop in binfmt_misc) is told so: turn it on in /etc/wsl.conf. A user's own Windows program is still refused, as before: its paths and variables wouldn't cross over. A Linux magpie in the distro isn't looked for: the Windows one reaches the gateway at 127.0.0.1 and reads this machine's settings, the same version as the app, which a Linux one would need the gateway's address from inside WSL for. With --wsl, magpie mcp image opens the paths the agent gives (its roots, reference images, where to save, ~/) as the distro's — /home/me/x through \\wsl.localhost\<distro>\home\me\x (\\wsl$\ where that isn't), /mnt/c/x as C:\x — answers the saved paths as the distro's (/home/me/app/generated-images/…, /mnt/c/…), and with no project falls back to the distro's ~/Pictures/Magpie. Tested on the Windows 11 test box: in a WSL2 Ubuntu 24.04 distro the library's probe gave /mnt/c/…/magpie.exe and interop:on, and this build, started from the distro as the library writes it (the .exe's /mnt/c path, mcp image --wsl, MAGPIE_GATEWAY through WSLENV to a stand-in gateway in the distro), answered initialize and generate_image with a root of /home/yeton/m900proj: a relative path and an absolute reference image (sent as /v1/images/edits), ~/m900proj/home.png and /mnt/c/… were each saved there and answered as the distro's paths. TestWSLMagpieImage (the /mnt/d command, --wsl, WSLENV, nothing rewritten or asked again at the next sync, a custom mount, interop off as a problem) and TestLinuxHome in internal/library, TestWSLPaths in internal/imagemcp are new and fail without the change; TestWSLServers (npx.cmd still refused) passes. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (internal/provider failed once under load and passes alone).
2026-10-05 20:09:34 +08:00
plugin, gateway, gui: a plugin can be gateway middleware — onRequest, onEvent and onResponse run in magpie's gateway on what agents send and get back (moejs) — a plugin could only be a subscription provider, run in the Bun host, a process away from the gateway. A plugin may now declare middleware (package.json magpie.middleware, or a *.middleware.js / .mjs file), run in-process by moejs (pure-Go JS engine): compiled once, one runtime per request from a pool, so every hook of a request shares ctx.state. onRequest(body, ctx) sees the agent's body in its own API before routing; what it returns is the body the next middleware and the gateway get, undefined leaves the bytes as they were, ctx.reject(status, msg) answers with an error in the agent's API's shape. onEvent(ev, ctx) sees each SSE event's JSON (export const events = [...] filters by name; an object changes it, null drops it, undefined passes the bytes through untouched; data: [DONE] is no JSON and passes by). onResponse(body, ctx) sees a whole JSON reply, with ctx.status. ctx has protocol, model, stream, path, agent, the plugin's options and state. A hook that throws, returns a non-object or runs past its limit (250ms request/response, 50ms an event, 1s load; interrupted) leaves the request or reply as it was and is counted and logged (60 lines a minute); a compressed reply passes as-is. Files are reloaded within a second of a change. Only agents' requests run through it (serveAgent at the gateway's agent entry points and the Codex backend), never magpie's own titles, classifier or stand-ins; it sees bodies before redaction. A middleware-only package is skipped by the Bun host. The Plugins page marks a middleware with a chip, its hooks, calls and µs per call (JSON in and out counted), failures in red with the last error, and a load error. en/zh/ja/de; docs/reference.md and usemagpie.ai/docs/plugins#middleware (en/zh/ja) with an alias example. Tested with a real DeepSeek account through magpie web --gateway in a sandbox HOME: an alias middleware served "fast" and named it in message_start, a reject came back as 403 permission_error, a Chat stream uppercased; real Claude Code (sandbox CLAUDE_CONFIG_DIR) did Read, Bash and Glob tool turns through three middlewares with 0 failures: ≈1–1.6µs an event, 0.2–0.7ms an onRequest that parses and rewrites Claude Code's ≈80KB body. Real use found two bugs fixed here: [DONE] parsed as JSON (SyntaxError on every Chat stream) and stats leaving out the JSON time. internal/middleware tests (chain, undefined keeps bytes, reject, fail-open on throw/spin/string with the runtime reused, async hook, load errors, off and Bun skip, reload, split/CRLF/filtered/dropped stream events, [DONE] untouched, whole reply and gzip, 64 in parallel; BenchmarkEvent ≈0.9µs, 11 allocs) and TestMiddlewareThroughGateway (alias routed, events by protocol, redaction still upstream, reject in Anthropic's shape; fails without the change) are new; plugin-middleware.test.cjs (Chromium and WebKit, en and zh: chip, hooks and stats, failures with last error, broken and off, no left border) fails without it. The other plugin GUI tests and click-scroll pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (internal/provider failed once under load and passes alone).
2026-10-05 19:14:31 +08:00
netproxy: a host on the user's own network goes around the proxy (#982, jiz4oh: a Remote magpie's model list failed with "no model list: http://server…:3425/v1/models: 502 Bad Gateway", and Test with "no model to try", while curl with the key got the list) — the 502 had no body, which magpie's own errors always carry, and the other magpie logged nothing for the refresh: the request went to the proxy magpie takes from its settings, the environment or the system (Clash and the like), which couldn't reach the LAN name and answered 502. netproxy.Func now sends a request directly when the global proxy would take it and the host is on the user's network: a private, link-local, loopback or 100.64/10 (Tailscale) address, a .local name, or a name whose every address is one of those (looked up for at most 400ms, kept a minute; a name that doesn't resolve still goes through the proxy). A provider's own proxy is still taken as it is. Models, chat, count_tokens and the shared quota cards to a Remote magpie all go through Func (http.DefaultClient and the gateway's transport), so all are covered; so is any other provider on the LAN (an Ollama on a NAS). TestLANModelListNotThroughProxy (a proxy that answers 502 to everything, set in magpie, and a fake remote magpie on this computer's LAN address) fails without the change with the reporter's bare "502 Bad Gateway" and passes; TestLANGoesAroundTheProxy (IPs, LAN/Tailscale names, a vendor, a provider's own proxy) fails without it. Not tried on the reporter's network; they're asked to confirm. go test ./internal/netproxy/ ./internal/catalog/ ./internal/provider/ ./internal/gateway/ ./internal/update/ ./internal/usage/ pass; -race -count=5 on the new tests; go vet, darwin/linux(nogui)/windows builds pass.
2026-10-06 13:35:32 +08:00
windows: an account list read while another magpie writes it is waited for, not taken for no accounts (Packing1 on Discord, Windows 11: 早上更新重启后之前登录的Antigravity订阅供应商和账号都没了 … 整行都没了,供应商里面antigravity直接消失了) — on Windows a file being renamed over (every write magpie makes: a temp file renamed onto it) is, for a moment of up to ~2ms, "The system cannot find the file specified" to anyone else looking at it, with MoveFileEx and with POSIX-semantics renames alike. filememo.Read gave that back as an error, readLogins as no accounts, and whatever change came next (any account's read-modify-write) wrote the empty list back: every built-in account gone, its subscription with it. Measured on Windows 11 (10.0.0.132): one process replacing a 200 KB file, another stat-ing and reading it, saw the file "not there" in a few windows each 20 s. Now steady.Stat and steady.ReadThere wait out a moment of a file not there (15 tries a millisecond apart, Windows only), and filememo looks through them, remembering a file found not there so one that really isn't costs the wait once. A magpie started by an update now waits for the one it replaces before its start-up moves write any file (AwaitPredecessor at the top of run, not only in the GUI). TestLoginsKeptWhileAnotherMagpieWrites (Windows: two writers replacing logins.json, readLogins never without the Antigravity account) failed 1 in 3 and 2 in 8 runs on the old code ("read as no accounts 8 times"), passed 11 of 11 on this; filememo's TestReadWhileReplaced the same (1180 misses on the old code); TestStatWaitsOutAReplace everywhere. A real update in a sandbox home on the Windows box, magpie-cli 0.1.880 → 0.1.942 from the release, kept the account and logins.json byte for byte. go vet (darwin, and windows nogui), builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 10:57:45 +08:00
gateway, provider tests: the tests stay off the network (testenv.Offline in their TestMains) — after 555f5706 (#1016) routing reads a plan key's quota windows, and a test whose provider has a vendor's real base URL and a made-up key would ask the vendor in the background. Routing's own read (keyAllowance, allowances) was already stubbed in routing_test.go's init, and a survey with a dial guard found no plan-key dial, but it found others: every plugin host's first start fetched models.dev's catalog (65 dials in the gateway's plugin tests), and provider tests asked api.github.com (TestSubscriptionUsageEachAccount, TestSubscriptionUsageServesStale), chatgpt.com (TestCopilotQuotaWithoutEditorsSignIn), server.codeium.com (TestDevinSignedInOnlyInMagpie) and decide.test (TestModelTest) with made-up accounts; each passed whether the vendor answered or not. internal/testenv's Offline wraps http.DefaultTransport's DialContext (so http.DefaultClient and every transport cloned from it later): localhost, *.localhost, loopback and unspecified addresses are dialled, anything else is refused before it is looked up, said once per address on stderr ("testenv: a test asked models.dev:443, refused: tests stay off the network"). internal/gateway's TestMain and internal/provider's (kiro_main_test.go, in RunIn's setup) call it before the tests run. TestLocal (the address table) and TestOffline (a loopback httptest server answers through DefaultClient; models.dev is refused through it and through a Clone made after the call) pass -race -count=3. go test -race -tags nogui -timeout 30m ./internal/testenv ./internal/provider ./internal/gateway passes under a temp HOME (gateway 516 s). The gateway and provider test binaries were then run under sandbox-exec with every non-loopback network-outbound denied, with the kernel's Sandbox reports streamed: both PASS, Offline refused only the six addresses above, and the kernel reported no denial, so no transport outside DefaultTransport reaches the network either. go vet, gofmt -l, linux and windows (nogui) builds pass. Tests only.
2026-10-06 21:08:57 +08:00
upstream: the vendors' own status pages say when their API is down (#971, emo172: an Anthropic or OpenAI outage looked like a sign-in or quota problem) — a provider that calls Anthropic's, OpenAI's or Moonshot's API (by its base URL's host: api.anthropic.com, which the Claude subscription calls too; api.openai.com and chatgpt.com, Codex's; api.moonshot.cn/.ai) carries "API outage" (red), "API degraded" or "API maintenance" beside its name on its Providers row and its Usage card while the vendor's page says so; its title names the parts and the open incidents, and a click opens the incident (or the page), not the row. internal/upstream reads each page's Statuspage-style summary.json (status.claude.com; status.openai.com, incident.io's; status.moonshot.cn), checked by hand on 2026-10-06, and only the API's own parts (Claude API and Claude Code, not claude.ai; Chat Completions, Responses, Codex, Codex CLI, not "Codex in ChatGPT Desktop", which was degraded that day); an incident naming no parts (OpenAI's) counts only while those parts are affected. Pages are read only when the Providers or Usage page asks (GET /api/upstream), at most once in five minutes, never on a request's way; a page that fails, isn't JSON or no longer lists the API's parts is unknown and marks nothing. A relay serving the same models is never marked. DeepSeek's page (Flashduty) has nothing readable and is only known for its link; OpenRouter, xAI, Mistral and z.ai have no page read. The gateway's failure messages, TUI, CLI and tray aren't changed. zh/ja/de added. upstream_test (fake pages from the real summaries: API down, claude.ai down isn't the API, Claude Code degraded, OpenAI's app isn't the API, 503/HTML/changed page unknown, read once in Fresh and Of never waits, hosts; -race -count=5) and gui's TestUpstreamRoute pass; upstream-status (Anthropic down: its provider and the Claude subscription marked, relay/OpenAI/unread Moonshot not, click opens the incident not the row, all well marks nothing; 900 and 440px, the badge whole, the name giving way) passes in Chromium and WebKit, en and zh, and fails 8/8 without the app.js change. i18n-keys, gui-ja, gui-de, click-scroll, account-return, account-models, usage-keys-fold, deprecated-subscriptions and add-sheet pass; go vet, go test ./internal/gui/, linux/windows/nogui builds pass.
2026-10-06 12:36:05 +08:00
wsl: background work never touches a WSL distro that has stopped since it was last seen running — it booted it again (TJHHHH on Discord: while they were repairing WSL, magpie kept starting it). Detection already listed only running distros, but the running set was kept for a minute (wslListAge), agents built while a distro ran kept its home and read its files from their Get/Sync/Check/Reached/Routed closures later, the sessions listing kept a distro's Running for 30s+ and read its \\wsl.localhost files, and wslrun.Find reused a tool it had found in a distro for a minute and ran wsl.exe -d on it. Any wsl.exe -d or \\wsl.localhost read boots a stopped distro, so a wsl --shutdown was undone within a minute, and the distro, running again, kept it going. Now wsl.exe -l --running -q (it starts nothing; UTF-16 with or without BOM, or UTF-8 under WSL_UTF8) is asked again after 3s, and a distro not in it is not touched: a WSL agent's Get gives the last values seen, Sync/Check/Reached do nothing and Routed/Joined/Beside/Added say no; sessions aren't listed or read from it (wslCold says cold); wslrun.Find (Claude subscription sign-in and usage) doesn't reuse or run a tool there; the Library gives skills to no target in it and its MCP probe says it isn't running. An explicit action still starts it: setting a WSL agent's field, a Set on a running agent, resuming a WSL session. TestWSLStoppedSinceNotOpened (agent, sessions), TestWSLStoppedSinceNoTarget (library) and TestFindStoppedSince (wslrun) fail without the change and pass; TestUp covers the -l --running parse. On the Windows box (10.0.0.132) a test binary that had seen magpie-fedora running ran Values/Check/Reached/wslDistros/wslAgents/sessions.List/wslrun.Find against it for 4 rounds while it was stopped: it stayed Stopped and wsl -l -v was unchanged; wslrun and agent WSL tests pass there. No distro was terminated for the check (magpie-desktop is in use, the stopped ones aren't ours). go test ./... (nogui), -race on the WSL tests, go vet, windows and linux (nogui) builds pass.
2026-10-06 14:25:41 +08:00