mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-11-01 17:09:55 +00:00
codex/audit-log-redaction
2823 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dac2c01b9f | Redact credentials from request and application logs (audit 10-29) | ||
|
|
786406e569 |
fix(cli): print the op_key of a failed write; stop claiming a re-run is safe (BUG-3999) (#2346)
* fix(cli): print the op_key of a failed write; stop claiming a re-run is safe (BUG-3999) Each CLI invocation sends a new idempotency key, so re-running a command after an unclear outcome is a new request. Failed writes that sent an op_key (create, comment, update/note/decide, patch, undo, push, copy) now wrap the error in cli.OpKeyError, and the root error writer prints the key on stderr with the --op-key re-run hint. Chat is excluded (caller-owned key). SKILL.md no longer says a re-run never applies a patch twice. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(cli): read APIError through the op-key wrapper at every call site (BUG-3999) A write that sent an op_key comes back wrapped in OpKeyError, which a direct err.(*cli.APIError) assertion does not see through: pad item create lost its plan-limit structured marker. Add cli.AsAPIError (errors.As) and use it at all 17 command-side sites. Add the plan-limit, --format json, decide and push tests. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(cli): keep the op-key hint when create replaces a plan-limit refusal (BUG-3999) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(skill): drop the false retry claim from the plugin's SKILL.md copy too (BUG-3999) plugin/skills/pad/SKILL.md is maintained separately from the embedded skills/pad/SKILL.md (it is not synced from it), and it still said a plain re-run "never applies it twice". It now carries the same corrected text, and the prose pin checks both files. Adds a test that a failed write which sent no op_key prints no hint (r1 H1, H2). Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v |
||
|
|
a5e275795e |
fix(web): label the new-item dialogs' dismiss button Close, not Cancel (BUG-4004) (#2345)
Both new-item surfaces, the desktop quick-add dialog and the mobile capture sheet, keep the dialog open after Add for the next item, so the dismiss button ends the session rather than undoing anything. "Cancel" read as if it would discard what was just added. The class names and the button order (TASK-3972) are unchanged; the desktop hint already says "Esc to close". Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v |
||
|
|
81248f33e8 |
feat(schedule): firing for item schedules, floating recipient zone, re-anchor and hooks (TASK-4102 PR1b) (#2344)
* feat(schedule): firing for item schedules, floating recipient zone, re-anchor and hooks (TASK-4102 PR1b) fireOneReminder gains a managed branch (unmanaged path unchanged): on Postgres one pin statement locks item, workspace and schedule before the arbiter takes the reminder; after the win the schedule is loaded and a floating occurrence is resolved in the recipient's zone, deferring (no event) when the row was armed early at the UTC+14 bound. A stored rule that no longer compiles leaves the row fired with no event and sets error_code. The schedule start is re-anchored to the fired occurrence under the plan's conditions. models.Reminder gains schedule_id and occurrence_at; the notification dedupe key uses occurrence_at for managed rows. The users.timezone writers and the assignee write reset the affected user's armed floating rows to the bound after they commit. Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS * fix(schedule): UpdateSchedule locks the item first; hook-failure and lock-order tests (TASK-4102 PR1b) UpdateSchedule takes the item FOR KEY SHARE (Postgres) before its schedule FOR UPDATE, so every writer runs item, schedule, reminder. Adds a test-only seam to fail the recipient hook, a test that a floating row fires in the new zone when the hook fails, a PG test that UpdateSchedule waits on the item before the schedule, and a comment on the fire pin's rowmark-order reliance. Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS * fix(store): compile a managed schedule's stored rule before the floating deferral (TASK-4102) A broken floating rule was deferred once before error_code was recorded. The compile now runs first, so it records error_code on the first fire attempt with no deferral write and no event. Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS |
||
|
|
c4ce74a6c2 |
feat(web): per-user time zone setting, browser default and mismatch notice (TASK-4101 PR2a) (#2342)
* feat(web): timezone types, api helpers and an identity-fenced authStore.setTimezone (TASK-4101) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(web): UI dismissals reset on identity change and fence late responses (TASK-4101) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * feat(web): browser time zone default (set-if-unset) and the mismatch notice (TASK-4101) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * feat(web): time zone setting in console settings (TASK-4101) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(web): fence the settings profile load, merge PATCH answers into the profile, and let explicit zone writes win over a late default (TASK-4101) PR2a r1, lead ruling ca031372: C-M1, H-L1 (both sites), H-L2, H-T. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(web): reload settings for the current identity, gate Save on it, and count only successful explicit zone writes (TASK-4101) PR2a r2, lead ruling bfc37793 (H2-M1, H2-M2, C2-L1) and the option-A ruling on the size stop: the four profile-writer fences (H2-L3) move to TASK-4129. The identity listener also clears the zone Save's busy and message state, which its fenced finally no longer does. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(web): a sign-out does not reload the settings profile (TASK-4101) The identity listener reloaded on user -> '' too. On account delete those /auth/me and /auth/tokens 401s fired the session-expiry redirect, which raced the delete's own navigation to /login (#2342 E2E, account-delete.spec.ts:99). Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v |
||
|
|
36f88b89e3 |
feat(schedule): item_schedules storage, managed reminder rows and store CRUD (TASK-4102 PR1a) (#2341)
* feat(schedule): item_schedules storage, managed reminder rows and store CRUD (TASK-4102 PR1a) Migrations 162/163 (SQLite) and 128 (Postgres): item_schedules, schedule_occurrences and item_reminders.schedule_id, with item_schedules.error_code (NULL = healthy; a schedule.Code or cap code). A schedule is the rule; its one outstanding occurrence is a managed item_reminders row. Store: CreateSchedule/GetSchedule/UpdateSchedule/DeleteSchedule behind one compileScheduleSpec door, fireInstant, recipient-zone resolve, floating schedules armed at the UTC+14 bound. RearmReminder/AckReminder/DeleteReminder refuse a managed row with ErrReminderManaged. Export skips managed rows. NUL triggers, migrated refusal list and purge census cover the new tables. Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS * fix(schedule): non-timing edits never recompile; lead edit cannot un-fire a row (TASK-4102 PR1a r1) - A non-timing PATCH (kind, audience, lead_minutes) no longer compiles the stored rule, so a schedule whose rule no longer compiles can still be edited and keeps its error_code. error_code clears only when a timing edit's merged spec compiles. - A lead change retimes an armed managed reminder from the stored next_wall and zone through fireInstant, with a conditional UPDATE (fired_at IS NULL) that never touches fired_at or acked_at, closing the tick/un-fire race. - CreateSchedule maps a foreign-key refusal after the liveness check to ErrScheduleItemGone (test hook pins the race). - The atomicity test now runs on Postgres via a plpgsql trigger. Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS * fix(schedule): dtend is not a timing edit; pin exactly-one-fire on the lead race (TASK-4102 PR1a r1) - A dtend-only update validates and writes dtend without recompiling, re-arming, or clearing override, anchor_wall, fired_at or acked_at. - The lead-vs-tick interleave test now drives FireDueReminders: one fire before the lead write, the row unchanged after it, and 0 on a second tick. Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS * fix(schedule): dtend is not a timing field; a dtend edit never recompiles or re-arms (TASK-4102 PR1a r1 H2) Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS |
||
|
|
7e5384ca07 |
feat(chat): refs in chat bodies, and the Chat entries in /console and the Sidebar (TASK-4044 PR 3c) (#2340)
* feat(chat): ref tokenising and the Go-parity trim for chat bodies (TASK-4044 PR 3c) refs.ts and refs.test.ts from the parked 98247acb8, with one deliberate divergence (D4): the three String.trim() calls are presence.ts trimSpace, matching Go's strings.TrimSpace on U+FEFF and U+0085. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * feat(chat): the thread collects ref verdicts from every page and send (TASK-4044 PR 3c) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * feat(chat): render visible refs as links and restricted ones as a placeholder (TASK-4044 PR 3c) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * feat(chat): the Chat entry in the /console nav bar (TASK-4044 PR 3c) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * feat(chat): the Chat entry in the workspace sidebar, gated on sign-in (TASK-4044 PR 3c) Option C, its own commit so it can be dropped alone. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * test(chat): e2e for refs in bodies and the Chat entries (TASK-4044 PR 3c) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * docs(chat): the refs disclosure names arrival-order merging (TASK-4044 PR 3c) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v |
||
|
|
e9cd8bcdc7 |
test(server): widen the OAuth-scope stream test's hang bound past context expiry (BUG-4126) (#2339)
The negative-control subtest opened the stream with a 3s context and
fataled at 4s, leaving the handler 1s to notice ctx.Done and unwind. A
slow scheduler tick in internal/server's ~370s CI run failed it at
|
||
|
|
d7da14bed4 |
feat(chat): the web chat write path: composer, send/retry/delete, a serialized thread store (TASK-4044 PR 3b) (#2338)
* feat(web): chat write path: composer, send/retry/discard/delete, message menu, draft keeper (TASK-4044) PR (3b) of the /messages/[n] thread. Optimistic sends keyed by op_key with same-key retry, reply targets, a hover/focus/long-press message menu (Reply, Copy, Delete), a visualViewport-aware composer, and a session-expiry draft keeper stashed per user and thread. Sizes are checked against the server's rules (Go TrimSpace, UTF-8 body bytes, the escaped 128 KiB request cap), and a refresh is deferred while a send is in flight. The parked client.ts and types hunks (H2, H3, H4, H7, H12) land byte-identical. Claude-Session: https://claude.ai/code/session_01FoPhLauGUBMCN1cRCbNi5b * test(web): each-key census entry for the thread's pending-aware list (TASK-4044) Claude-Session: https://claude.ai/code/session_01FoPhLauGUBMCN1cRCbNi5b * fix(web): re-anchor at the newest page on a send beyond a gap; cut Ctrl/Cmd+Enter (TASK-4044) Pre-r1 ruling b3dd1e54: a confirmed own send while a gap is open used to be discarded, so the sender's bubble vanished. The window now moves to the newest page (store #reanchor, epoch-fenced against older loadNewer/loadOlder/refresh answers) and the Q-d scroll applies. The Ctrl/Cmd+Enter send gesture is cut (follow-up TASK-4099). Claude-Session: https://claude.ai/code/session_01FoPhLauGUBMCN1cRCbNi5b * fix(web): an out-of-order re-anchor keeps the window a later send moved (TASK-4044) 3b r1, hunter finding 2: two own sends confirming beyond a gap both re-anchor. When the first one's newest-page fetch landed last, it replaced the window that the second one's (newer) page had already set, and the second sender's row vanished. #reanchor now captures the epoch before its fetch; if another re-anchor landed meanwhile, it keeps that window and merges its own row only where it is contiguous. Also pins hunter finding 1 (refuted): a peer row committed while a send is in flight is brought in by the deferred refresh's sweep leg. Claude-Session: https://claude.ai/code/session_01FoPhLauGUBMCN1cRCbNi5b * fix(web): an own send past unfetched peer rows no longer hides them from the next refresh (TASK-4044) 3b r1, hunter finding 1 (re-ruled FIX in a61ca213): merging a confirmed own row in the no-gap branch moved newestSeq past peer rows committed before it, so the follow-up refresh's step 1 started after them and only the newest-200 sweep could bring them in. More than 200 such rows left a silent hole, which falsified the header's R1 invariant. The no-gap branch now sets a one-shot cursor floor at the pre-send newest and marks the thread dirty; step 1 starts from the floor and clears it. #reset and #reanchor clear it too. Test: 201 peer rows committed during one send all arrive (mutant B18). Claude-Session: https://claude.ai/code/session_01FoPhLauGUBMCN1cRCbNi5b * fix(web): serialize the floor, the late re-anchor merge and the delete merge against the moving window (TASK-4044) 3b r2 (ruling a669a3a4), three confirmed findings: - H1: a refresh in flight cleared a cursor floor that a send set meanwhile. The refresh now clears only the floor it started from. - C1/H2: a late re-anchor checked contiguity at the window's oldest only, so a newer own row merged past unfetched peer rows. The no-gap branch and the late re-anchor now share #mergeOwn, which floors past skipped peer rows. - H3: a delete answer that landed after a re-anchor inserted the tombstone below the new window. It merges only when the window holds that seq. Tests: one '3b r2 ordering' row per finding (mutants B19-B21). Claude-Session: https://claude.ai/code/session_01FoPhLauGUBMCN1cRCbNi5b * refactor(web): serialize chat thread window ops and keep the published window contiguous (TASK-4044) One promise chain runs every op that fetches then writes the window (open, loadOlder, loadNewer, refresh, an own row's placement, a delete's tombstone). An own row past unfetched peer rows is reached by #fill (bounded); on failure or the bound it detaches and renders after "Show newer messages". Replaces #epoch, #floor, #dirty/#settled's re-run and the late re-anchor branch; the in-flight refresh deferral moves onto the queue. Adds a seeded property test. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): property test pins when an own row may detach and how far a fill pages (TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): pin that Show newer does not merge a page carrying an in-flight send's row (TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): default the property walk to 100 seeds to stay under the CI budget (TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): let the property test's settle page to the end of a 15k-row server (TASK-4044) The settle loop capped at 30 Show-newer pages; seed 1171 ends with 7,626 rows and the window at 804, which is the harness running out of pages, not a store defect. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * fix(web): disable Show newer while a send is in flight (TASK-4044) Derived from the pending items on the page, no store change. Also corrects the property test title to I1-I10. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): pin the two in-flight-send guards against a merge beside a sending bubble (TASK-4044) Two deterministic store tests for invariant I9 (ruling 2a03a442): N12, the own apply must not fetch while another send is in flight, and N14, a refresh page that lands during a send must not be merged. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * fix(web): keep the window when the re-anchor fetch fails and recheck sends in #fill (TASK-4044) C1 (ruling ec6bbe31): a failed newest-page fetch detaches the row and keeps the reader's window and hasNewer, instead of replacing the window with the sent row. H1: #fill returns false after an await while another send is in flight, so the row detaches rather than land beside the bubble of a retried committed send (I9). Adds the red-first tests for both and four tests pinning the deferred-refresh release after a confirm, a refusal, a failure and a Discard. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): make the property walk less vacuous (TASK-4044) I10 causes are recorded per sent row, the walk discards failed bubbles, and refusals are thrown as errors the real predicates (reply target, too_large) recognise instead of mocking them to false (3b r4 C2/H4). Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): e2e pin that a detached send shows below the Show newer marker (TASK-4044) Q3 (ruling ec6bbe31): open past a gap, fail the re-anchor read, send, and assert the sent row is below the marker while the marker stays. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * fix(web): a tap opens no message menu; focus, callout and live regions (TASK-4044) r5 H1 + Codex M: the bubble opens its menu on :focus-visible only (a tap focuses it but must open nothing), and a press inside the bubble but outside the menu unpins. H2: E5c writes its screenshot under testInfo.outputPath. H3: Reply focuses the composer; Copy and Delete hand focus back to the bubble. H4: -webkit-touch-callout none and contextmenu preventDefault while a touch press is armed. H5: the composer and pending bubble status regions stay mounted and only change text. E6 gains a REAL tap(). Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * fix(web): a replayed send does not overwrite a held row; pin the re-anchor mark (TASK-4044) r5 A2: #applyOwn prefers a row already held (window or detached) over the answer, and the re-anchored page over the answer, so a tombstone wins (I5). A1: pin markRead(newest) after a re-anchor, the ruled behaviour. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): property walk attributes rejections, counts paths, checks the server marker (TASK-4044) r5 A3: 'rejected' is recorded for the bubble whose own apply fetch failed; the run counts detach, re-anchor, fill-resolved and fill-detach hits and fails on any zero; I6 is checked against the server's recorded read marker. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): property walk counts a re-anchor from an empty window (TASK-4044) The deep run (seed 2330) found the I6 skip rule blind to a re-anchor that starts from an empty window: the window start goes 0 -> N, which the moved-detector read as no move. A start above 1 from an empty window is the re-anchor. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * fix(web): a replayed send below the moved window is history (R1, TASK-4044) r5a2 R1: in #applyOwn a sent row whose seq is below the window's oldest row is dropped with its bubble instead of merged across the hole (I1), the rule the re-anchor branch already follows. Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * test(web): property settle compares the whole window row for row (R4, TASK-4044) r5a2 R4: I3 compared body and deleted for the newest 200 rows only. It now compares every row, allowing only the disclosed residual (a live row older than the newest 200 that the server has since retracted). Claude-Session: https://claude.ai/code/session_01Pa22hLXP49jmY9HVCfbfy6 * fix(chat): trimSpace by index scan, linear on an interior whitespace run (B2-M1, TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(chat): a detached own row keeps hasNewer after the apply (A-L1, TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * test(chat): a held detached tombstone beats a replayed answer (A-M1, TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * test(chat): the walk commits and loses an answer, then replays; overlap tagged at the op (A-M1, A2-L1, TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(chat): the reply bar clears on a successful delete; Retry/Discard refocus the composer (L3, L4, TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(chat): an unanswered send's body joins the session-expiry stash (B2-L1, TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(chat): detached rows sit in a flex column so own rows right-align; e2e waits for pending=0 (L1, L2, TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * test(chat): exempt a lost-answer key from I9 only once its answer is rejected (r6fix F-L1, TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * perf(chat): hoist the White_Space regex out of trimSpace (r6fix R-L4, TASK-4044) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(chat): a refused Retry puts its text back in the composer with the notice (r7 M1, TASK-4044) A Retry refused because the quoted message was deleted (or is missing) discards its bubble, and the page ignored the outcome, so the body was lost with no notice. Composer exports the refusal tail send() already had as restore(), and the page calls it for a refused Retry. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v |
||
|
|
e4a5c20dd0 |
feat(schedule): internal/schedule, RFC 5545 RRULE expansion with pinned/floating time and DST rules (TASK-4100) (#2336)
* feat(schedule): wall times, DST resolution, typed errors and a strict RRULE parser (TASK-4100 1/2)
Commit 1 of 2 of the internal/schedule package (pure, no store/HTTP).
- wall.go: Wall, ParseWall, String, Resolve. Resolve does not use
time.Date's gap/overlap choice: a gap takes the offset before it
(shifts forward), an overlap takes the earlier instant.
- errors.go: ErrMalformed, MalformedError with typed Codes, ErrViewRequired.
- rule.go: strict RRULE parser (ParseRule, canonical String, combination
and satisfiability checks, Validate against Start). FREQ=SECONDLY is
refused (CodeUnsupported); a 512-byte, INTERVAL 1e6, years 1970-2200
limit set applies.
- doc.go: the DST and sub-daily policy, including the population table.
Size (wc -l): production 652 raw (doc 44, errors 82, wall 106, rule 420),
515 excluding comments and blank lines; tests 388 (resolve 107, rule 219,
wall 59, tzdata 3).
RED controls: Resolve overlap taking the later instant fails TestResolve
(ny-overlap-first-minute, ny-overlap-mid, london-overlap,
lord-howe-overlap); removing the SECONDLY refusal fails
TestMalformedRulesTable/FREQ=SECONDLY.
Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL
* feat(schedule): the expansion engine on rrule-go, caps, shorthands and RFC 5545 conformance (TASK-4100 2/2)
Adds github.com/teambition/rrule-go v1.8.2 (go.mod +1 line, go.sum +2) behind
a wrapper. The library is a naive wall-clock engine fed UTC-labelled walls;
Resolve maps each wall to an instant. Bare HOURLY/MINUTELY step in elapsed
time with a fast-forward; everything else is wall clock with collapse.
- spec.go: Spec, Compiled, Compile, Occurrence. Every refusal is a
*MalformedError; no library text can surface.
- engine.go: NextAfter, Between, StartSynced. Resolve is not monotonic inside
a DST gap, so the walk is bounded on the WALL reading (48 h margin), never on
"first instant >= to"; results are sorted, de-duplicated by instant, strictly
increasing, Between half-open.
- caps.go: Limits, DefaultLimits, CapError, ErrCapHit, the CapTime guard and
the CapScan precheck. The library iterator is used, never All().
- shorthand.go: the eight shorthands and NormalizeRule.
- CLAUDE.md: one Key Directories line.
Tests: the 43 RFC 5545 3.8.5.3 rows and 30 DST/edge rows run through the real
package, the DST population table, caps, floating, Next/Between, shorthands,
TestOutputStrictlyIncreasingAcrossDST and TestCompileNeverReturnsLibraryError.
Erratum: the HOURLY example with UNTIL=19970902T170000Z is RFC 5545 erratum
3883 (170000Z is 13:00 EDT, so 15:00 EDT is past UNTIL). It stays in the suite
as an expected-different row asserting 09:00 and 12:00 EDT, never a skip.
Friday-made-on-Tuesday (ruling Q1): the Tuesday is not emitted and
StartSynced() is false.
Size (whole unit, commit 1 included), production only, code lines excluding
comments and blanks / raw: caps.go 81/109, doc.go 1/44, engine.go 225/265,
errors.go 60/82, rule.go 376/420, shorthand.go 41/53, spec.go 103/128,
wall.go 78/106; total 965 code, 1207 raw. Test files 1640 raw (row data
included).
govulncheck v1.2.0 (local binary, DB 2026-10-27) on ./internal/schedule/: No
vulnerabilities found.
RED controls, each applied then reverted: (a) no adjacent-instant dedupe fails
the gap-collision rows, TestBetweenDedupesByInstant and
TestOutputStrictlyIncreasingAcrossDST; (b) local-zone times fed to the library
fail dst-daily-ny-gap and the other gap rows; (c) emitting a non-matching
DTSTART fails TestNextAfterUnsyncedStartSkipsStart; (d) skipping the final
sort fails TestOutputStrictlyIncreasingAcrossDST (daily-rdate-inversion).
Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL
* chore(schedule): vendor teambition/rrule-go v1.8.2 verbatim into internal/schedule/internal/rrule (TASK-4100 H1, import)
Byte-for-byte copy of rrule.go, rruleset.go, str.go, util.go and LICENSE
(upstream 429348ca4477691b08edb0200b7f837edd32414c), plus PROVENANCE.md.
Not yet imported; no go.mod change. sha256, module cache vs vendored:
3af19c18...8fda rrule.go (cache == vendored)
89f1c9ca...ad8 rruleset.go (cache == vendored)
0dc2d364...7d96 str.go (cache == vendored)
d95c565b...0173 util.go (cache == vendored)
2c5aa800...470f LICENSE (cache == vendored)
Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL
* fix(schedule): bound rrule expansion with an in-loop step budget and drop the abandoned goroutine (TASK-4100 H1)
- internal/schedule now imports the vendored internal/rrule; go.mod and
go.sum drop github.com/teambition/rrule-go (go mod tidy).
- Vendored generator: a step budget (RRule.SetBudget / Exhausted) counted
on every period-loop and inner-skip iteration, matched or not. On
exhaustion the iterator finishes and records it; no panic.
- Limits.MaxSteps (default 200000) feeds it; exhaustion is a CapScan,
never a partial result. A 200k-step call costs about 13ms.
- guard no longer starts a goroutine: all work runs in the caller.
CapTime is now a wall-clock check between yields.
- probeSynced treats an exhausted probe as unsynced.
- doc.go says why the library is vendored.
- Tests: TestBudgetFeb29Minutely, TestBudgetUnsatisfiable (goroutine
count, wall time, CapScan); TestCapTimeGuard rewritten in-goroutine.
Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL
* test(schedule): prove no goroutine outlives an expansion call (TASK-4100 H1)
TestNoGoroutineOutlivesCall: MaxDuration 1ms, MaxSteps 2M on the
unsatisfiable rule (about 65 ns a step, so about 130 ms a call, under the
roughly 2.9M-step walk to year 9999, so CapScan still trips first). 20
calls, then NumGoroutine is read with no settle and must equal the count
before.
Mutant history: restoring the goroutine + select + time.After path in
guard stayed green alone at 58bc2a9b3, because the budget bounds the
abandoned goroutine. With this test it is RED:
budget_test.go:116: goroutines 2 -> 22 right after 20 calls; none may outlive a call
go.mod and go.sum have zero net diff against origin/main
|
||
|
|
b8b610812c |
feat: a per-user time zone for calendar days, backend (TASK-4101 PR1) (#2337)
* feat(users): users.timezone column, internal/usertz and the conditional set-if-unset write (TASK-4101) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * feat(auth): timezone on /auth/me and the session user, PATCH with if_unset, tz-mismatch dismissal keys (TASK-4101) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(server): compute overdue, standup and stamped dates in the viewer's zone, UTC when unset (TASK-4101) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * test(server): regenerate the cloud contract goldens for the additive timezone field (TASK-4101) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(store): add the Postgres twin of the users.timezone migration (TASK-4101) pgmigrations has its own numbering; 160 alone left the column missing on Postgres, and make test-pg failed on 'column "timezone" does not exist'. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * feat(store): NUL invariant triggers for users.timezone (TASK-4101) The census exists so no single validator is the only guard (ruling 1431c980 item 1): protect the column with its own post-160 trigger file instead of excluding it. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(server): decode a dismissal key once, and test the viewer zone through /me/needs (TASK-4101) A double-encoded %252F reached the handler already decoded once (net/url keeps no RawPath for it), and the second PathUnescape read it as a slash. Unescape only when chi routed on RawPath. Adds the unescape cases, the too_many_dismissals code assertion, an HTTP-level test that a zone ahead of UTC makes an item overdue on /me/needs, and the api.md notes for the cap and the additive dashboard/standup fields (ruling 1431c980 items 2-5). Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(server): the standup test expects the UTC day, and the zone note names no PR2 surface (TASK-4101) The standup date is the viewer's zone, UTC when unset; the test still compared against the host's local day and failed on any non-UTC host when the two dates differ. The note pointed at the web settings and `pad auth timezone`, which land with PR2 (ruling 00877b60). Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v |
||
|
|
d36f11cf40 |
fix(server): a workspace whose owner no longer resolves never yields a '//' path (BUG-4050) (#2334)
* fix(server): a notification whose workspace owner no longer resolves has no deep link, never a '//' path (BUG-4050) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(needs): a needs row whose workspace owner no longer resolves has an empty path and no link, never '//' (BUG-4050) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * fix(needs): fail the request on an owner lookup error instead of reading it as no owner (BUG-4050) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * test(needs): an owner lookup error must answer 500, not merely not-200 (BUG-4050) Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v * test(needs): rebind the owner-corruption UPDATEs so they run on Postgres (BUG-4050) The raw ? placeholders were a syntax error under make test-pg, so both postgres subtests failed before reaching the handler. Claude-Session: https://claude.ai/code/session_01Pa22hHHb1atXYf1JmBYE1v |
||
|
|
875bbaa24e |
feat(attachments): sandboxed in-pane PDF viewer with page navigation (TASK-4075) (#2333)
* build(web): pin pdfjs-dist 6.4.299 for the in-pane PDF viewer (TASK-4075) Exact pin, no overrides. The lockfile gains @napi-rs/canvas and its 11 platform packages: pdfjs-dist declares it as an optional dependency for its Node canvas, npm records the ideal tree for every platform, and nothing in a browser build reaches it. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * feat(web): 'pdf' surface renderer, exact MIME predicate, 25 MiB cap and bytes loader (TASK-4075) canPreviewAsPdf admits exactly application/pdf. PDF_PREVIEW_MAX_BYTES is its own 25 MiB constant with the measured receipt. createViewerBytesLoader is the text loader's sibling: same phases, same two size gates, same staleness fence, but it delivers bytes. Corrects the display.ts comment that called the text set 'deliberately outside' the server mirror, which text/plain never was. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * feat(web): AttachmentPdfPreview, a canvas-only pdf.js page viewer (TASK-4075) pdf.js is imported on mount (its own lazy chunk) with useWasm:false, no url, no scripting and a copy of the bytes (getDocument detaches its buffer). The worker is our own Vite asset. A Vite plugin copies only the two no-wasm JPX/JBIG2 fallbacks to a fixed path. DPR is capped at 2 and each page at 16 MP. No text layer, no annotation layer, no PDF-derived markup. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * feat(web): the Lightbox PDF arm (TASK-4075) A third byte-loader arm beside text/table: bytes loader armed, the other two disposed; loadKey carries the resolved size; the stage gives up its touch-action claim; the wheel exclusion and arrow guard cover the PDF scroller; PageUp/PageDown turn pages while Left/Right stay gallery paging. Too-large and error reuse the text arm's copy and Retry. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * fix(web): leave the desktop toolbar room above the PDF arm (TASK-4075) Found in the browser: the toolbar floats over the stage's top centre and intercepted clicks on the PDF's own page controls. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(web): PDF viewer browser specs, emitted-bundle scan, fixtures (TASK-4075) E1-E5 against the built binary, with the CSP instrument placed in the pdf.js worker's own scope and a control leg that forces wasm back on. U8 scans the emitted chunks for eval/Function call sites and @napi-rs. The two real JPX and JBIG2 fixtures come from pdf.js's test corpus (Apache-2.0), origin beside them. The fallback-arm specs that used a PDF as their no-renderer file now use a ZIP, as the fixture's own note said they would. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(web): U3 per-arm loader disposal and the late-size re-decision (TASK-4075) Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(web): settle the PDF e2e legs against the built binary (TASK-4075) A PDF tile emits a single-entry set, so the gallery legs became inert-arrow and no-sideways-movement legs (gallery paging stays a unit test). The over-cap leg reports the size through the attachment list, since the 25 MiB upload cap equals the 25 MiB preview cap. Dialog and popup instruments get control legs. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(web): pin the loader's id fence and silent AbortError (TASK-4075) Two mutants survived the first run: dropping the id half of the staleness fence (only a completion after dispose() distinguishes it from the token half) and dropping the AbortError check (only an abort the loader did not issue reaches it). Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(web): scope the E2 request check to other origins (TASK-4075) The first gate run caught my own over-broad assertion: with the whole attachment suite running in parallel, the app's background items-changes reads landed inside the click window. What the leg pins is that nothing leaves the server's origin. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * fix(web): bound the PDF viewer's reads, images and canvas sides (TASK-4075 r1) F1: one shared bounded byte reader (boundedBytes.ts) behind the PDF bytes loader and the text loader: Content-Length precheck, no whole-buffer fallback (a non-stream body is the error phase), residual one-chunk overshoot disclosed. F2: pdf.js maxImageSize 64 MP. F3: pageRenderSize returns null for non-finite or non-positive input. H1: PDF_MAX_SIDE 16384. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * docs(web): maxImageSize bounds every PDF image except JPX (TASK-4075 r2, H2) Claim correction only, no runtime change. pdf.js checks the dictionary W/H (pdf.worker.mjs:34283-34289); for JPXDecode the size is then re-read from the codestream (:33055-33061, :33084). pdf.js requests a reduced decode sized to about 2^30 bytes of output; the decoder's peak allocation is unmeasured. The real bound is TASK-4106. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(web): make the U8 @napi-rs scan cheap and give it an explicit timeout (TASK-4075 r2, S1) Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(e2e): an embedded TrueType font draws under the real CSP, with a stripped-program control (TASK-4075 r2) Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(web): record the measured U8 scan duration (TASK-4075 r2, S1) Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(attachments): E6 asserts the font-load-failure warning absent, with a rejecting-FontFace control; widen U8 scan (TASK-4075 r3) H3: collect console warnings for E6 only (opt-in), assert no 'Warning: Failed to load font', add a permanent control leg injecting a rejecting FontFace. Test name and comment claim only what is proven; font-src vs ArrayBuffer FontFace is unmeasured. H4: record the empirical >10% ink threshold (4375 vs 5204, 19%). H5: U8 also flags eval?.(, Function?.( and tagged-template Function; tripwire wording, CSP named as the wall. Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(e2e): E6 blank-stripped page must paint, claim only no font-load failure, control reads at the same point (TASK-4075) Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * fix(web): page-number box takes 0/-/+/=; clear stale canvas on bad page size; test pins (TASK-4075 r5) Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * test(web): L2 pin observes the canvas box reset (TASK-4075 r5) Claude-Session: https://claude.ai/code/session_01X2LCH8E34w8ma8v9Lk24Bp * fix(web): pin the open-effect renderFailed reset and drop the redundant phase guard (TASK-4075 r5 survivors) This applies the lead's day-40 ruling on the two fix5 mutant survivors. The analysis is on the TASK-4075 trail. (a) l2-renderFailed-reset-removed: a reachable window exists, so the line stays and is pinned. A new document whose scroll region measures 0 wide never reaches the render effect's own reset. Without the open-effect reset, the old document's "couldn't be drawn" alert stays over the new document. The new unit test fails with that line removed (AssertionError on the stale alert). (b) rekey-phase-guard-only-removed: no reachable state, so the clause is removed. The preview mounts only in the ready branch, and svelte's bind:this nulls pdfPreview in the same flush that unmounts it. There are no outro transitions, so a keydown can never see a non-null preview with the phase off ready. The two re-key tests stay as the pins on the null guard. Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL * fix(web): keep an over-cap read "too large" when the stream rejects the cancel, and make E2's link clicks land (TASK-4075 r6) This applies the lead's day-40 r6 rulings. The triage and the rulings are on the TASK-4075 trail. r6a2-L1: once the streaming count passed the cap, readBoundedBytes awaited reader.cancel() with no guard. An errored stream rejects that cancel, so the read threw and the PDF viewer showed the generic "couldn't be loaded" instead of "too large". The bytes were still refused either way. The cancel is now best effort, the same shape as the Content-Length precheck's body.cancel(). That makes it 3 cancel sites in the change: two stream cancels, both now guarded, and pdf.js's synchronous RenderTask.cancel, which has no rejection path. There are two new tests: one at the reader, and one at the loader with a real ReadableStream whose cancel rejects. Both fail with the guard removed (the loader reports 'error', not 'too-large'). r6a-L2: E2 clicked at 0.87 of the canvas height. The JavaScript link spans 0.747-0.874, and nothing checked that the point was in view. The clicks now go to each link rect's centre, scrolled to the middle of the scroll region, and the test first asserts that elementFromPoint at that point is the canvas. These clicks prove no more than the source pins do: the viewer has no annotation layer. r6a-L3: fix the indentation of the zoom test's closing brace. Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL * test(web): say exactly what the rejected-cancel stream demonstrates (TASK-4075 r7-codex-L1) Comment only. The test's stream has an underlying cancel that rejects; it is not an errored stream. An errored fetch body is one way a cancel rejects, which the comment now says instead of claiming the setup reproduces it. Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL * docs(attachments): state boundedBytes' real peak memory (TASK-4075 r8) Comment only. The header said peak memory is "at most max plus one chunk"; that holds only on the refusal path. An accepted read holds the chunks and the joined copy at once (about 2 x max), and the PDF caller's .slice() handoff adds a third copy (about 3 x max) until the chunks are collected. Lead-ruled fix-now on the r8 triage (H-L3), under the comment-only protocol. Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS |
||
|
|
b0accdca9c |
chore(deps): give @tiptap/* its own dependabot group, out of the npm tooling group (TASK-4107) (#2330)
dependabot #2327 bumped the three Tiptap packages (3.31.4 to 3.32.0) inside npm-minor-and-patch, alongside unrelated tooling, and went red. CLAUDE.md requires @tiptap/core, @tiptap/extension-collaboration and @tiptap/y-tiptap to move together, exact-pinned, and never in a tooling group. The new npm /web group `tiptap` (patterns @tiptap/*, no update-types, so majors stay coordinated too) is listed first, because dependabot assigns a dependency to the first group that matches it. npm-minor-and-patch also excludes @tiptap/*, so a later reorder cannot put Tiptap back in the tooling group. Built by Codex (gpt-6.1-sol) from the seat's brief; reviewed and verified by the seat. Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL |
||
|
|
6dfc431402 |
fix(deps): bump golang.org/x/net to v0.62.0 for GO-2026-6866 and GO-2026-6860 (TASK-4115) (#2331)
* fix(deps): bump golang.org/x/net to v0.62.0 for GO-2026-6866 and GO-2026-6860 (TASK-4115) govulncheck fails the Go and Nix CI gates on every PR. golang.org/x/net v0.61.0 (indirect) has two advisories, and pad's code reaches both: GO-2026-6866 (html.Render does not check every ancestor namespace transition when rendering raw text) and GO-2026-6860 (html.Parse/ParseWithOptions take quadratic time when coalescing text nodes). Both are fixed in v0.62.0, published 2026-10-27T19:00:16Z, after main last went green. go get golang.org/x/net@v0.62.0, then go mod tidy. Only x/net moves: one go.mod line and two go.sum pairs. No other module changes. govulncheck -mode binary on cmd/pad built from this tree reports "Your code is affected by 0 vulnerabilities", exit 0. The control, the same scan on a base build (x/net v0.61.0), reports both advisories, exit 3. v0.62.0 is younger than the 7-day dependency-age threshold, so it is allow-listed in .github/dep-age-allow.txt as a security fix citing both advisories. The nix vendorHash follows from the PR's Nix job. Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL * chore(nix): vendorHash for golang.org/x/net v0.62.0 (TASK-4115) Taken from the got: line of this PR's Nix job (job 122632750043, 04:34:55Z): specified sha256-uEIFUop7X4VHmbICrH6CW7SL1GB4tjmIIcHGKLkYmUU=, got sha256-ykiFPHwGu7Lr4NSJs45HuKj7rqfsBDUKtjiCTDfYkiA=. The job recomputes the hash before building, so its green result alone did not show the committed value was current (CONVE-3888). Claude-Session: https://claude.ai/code/session_01Pa22hK3vXbxQYYyvvvRGYL |
||
|
|
3e5cde148e |
chore(nix): heal vendorHash for the module set in aaea719b3b (TASK-2954)
|
||
|
|
aaea719b3b |
chore(deps)(deps): bump github.com/go-chi/chi/v5 (#2326)
Bumps the go-minor-and-patch group with 1 update: [github.com/go-chi/chi/v5](https://github.com/go-chi/chi). Updates `github.com/go-chi/chi/v5` from 5.4.0 to 5.5.0 - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.4.0...v5.5.0) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-version: 5.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
7cd91634dd |
feat(web): read-only chat thread at /messages/[n] (TASK-4044) (#2329)
* feat(web): chat message wire types, listMessages, markRead and the window model (TASK-4044) Carries the parked chat hunks the read-only thread first uses: ChatMessage, ChatQuote, ChatRef and ChatMessagesResponse, api.chat.listMessages and markRead, and the whole chat/model.ts with its test. ChatRef lands here because ChatMessagesResponse refers to it. ChatConversation.kind narrows to 'direct' | 'group' (migration 158 CHECK); a plain .test.ts pins it because svelte-check excludes *.svelte.test.ts. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(web): chat agent-name trim matches Go, and a production chatStore.stop() is inert (TASK-4044) normalizeAgentName now trims the White_Space property like Go's TrimSpace instead of JS trim(), which also trims U+FEFF (BUG-4092). A test-only Go row pins the same five inputs. chatStore.stop() now outdates an in-flight answer and makes onHint and resync no-ops until start() (rulings f05fd7c4, db5556a5). Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(web): the read-only chat thread at /messages/[n] (TASK-4044) Ordered by seq, with an "N new" divider frozen at the read marker as of open, Jump to unread, a quote chip that shows the excerpt only, a "sent from" label, and a window that follows the server live: a refresh pages new rows from the newest end (never missing one in a window of any size), takes tombstones from the newest 200, and never appends past the gap of a long unread run. Marks read only from the bottom sentinel on a visible tab, with the displayed seq. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(web): e2e for the read-only chat thread against the real server (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(web): fix the e2e fixtures' mine expectation and the read-count bound (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(web): the e2e opens after the read marker, so seq 1 is one Load earlier away (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(server): write the trim-parity inputs as escapes, not literal characters (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(web): census entry for the thread's seq-keyed each (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(web): close the chat thread on an identity change; drop E8's unfalsifiable 429 leg (TASK-4044) r1 H1: #reset() now clears #n and sets status 'loading', so a version bump after an identity change fetches nothing as the new identity. A swap already reloads the tab and a sign-out navigates away; this is defence in depth for a consumer that does neither. U14b pins it. r1 C2: the e2e server runs with PAD_DISABLE_RATE_LIMITS, so E8's "no 429" assertion could never fail. It is removed and the title says what the spec pins (reads per message); the burst is checked against real limits out of band. Claude-Session: https://claude.ai/code/session_01Y4xxtVpSpGmKykhWznQEdq * fix(web): re-mark rows that land during an in-flight mark-read; E8 counts both tabs (TASK-4044) r2 hunter: the "window grew while in flight" re-mark was queued as a microtask inside .then, so it ran before .finally cleared #marking and returned early. It now runs in .finally, and only when the mark succeeded and the window grew past the seq just marked. U17b-d pin the retry and both gates. r2 Codex: E8's per-tab bound was asserted on one tab only. Both tabs are counted now, each at 10*4+4. Claude-Session: https://claude.ai/code/session_01Y4xxtVpSpGmKykhWznQEdq |
||
|
|
a8af168304 |
fix(cli): session liveness reads unknown, not dead, across a PID namespace (BUG-4090) (#2328)
* fix(cli): session liveness reads unknown, not dead, across a PID namespace (BUG-4090) The session registry's verdict probed the owner pid with kill(pid, 0), and a pid means something only inside one PID namespace. From a sandbox or container shell (bwrap --unshare-pid) every host pid is ESRCH, so `pad session list` read every live host session dead, and `pad session prune` - and the prune that follows every `pad session register` - deleted their records. Records now carry pid_ns, the namespace the owner pid was captured in, stamped only when that pid is signalable there. The reader compares: across a boundary the pid is not probed and a missing socket is not evidence, so the verdict is unknown (kept by prune, untrusted by the arm gate). Only a socket answering with a different identity stays dead. A row without pid_ns read from a non-init namespace keeps a positive pid probe and turns a negative one into unknown. Headless arm-state files carry pid_ns too. Claude-Session: https://claude.ai/code/session_01BpZwJcFxRtM9Wax4fHhDNC * fix(cli): judge a session's socket by its mount namespace (BUG-4090 r1) The socket half of the liveness verdict now keys on the mount namespace the socket was seen in (mnt_ns, stamped only beside a socket stat'ed with its identity). A reader with another filesystem view gets unknown for a missing or different node; a row without mnt_ns keeps the old rule, so a container reading its own rows (or the Nix sandbox) behaves as before. The unverifiable prose now says "from here (this platform, or another namespace)", and the bwrap test gains a leg for a pre-fix, unstamped row. Claude-Session: https://claude.ai/code/session_01BpZwJcFxRtM9Wax4fHhDNC * fix(cli): a foreign-namespace reader never manufactures dead; chat direction stamps mnt_ns (BUG-4090 r2) An unstamped row read from a non-init PID namespace now reads alive only on a start-token match: a mismatch, a vanished pid or a zombie at that number describes the reader's namespace, not the owner's, and is unknown. The chat-direction file, the third socket-identity writer, records its socket's mount namespace like the registry and the arm file. The help and CLAUDE.md no longer overclaim for rows written before this version. Claude-Session: https://claude.ai/code/session_01BpZwJcFxRtM9Wax4fHhDNC * fix(cli): an unidentifiable namespace is a possible boundary on Linux (BUG-4090 r3) A reader that cannot read /proc/self/ns/{pid,mnt} (a sandbox with no procfs) no longer treats an unstamped row as same-namespace: its ESRCH or missing socket is unknown, not dead. A Linux writer in that position stamps pid_ns/mnt_ns with the sentinel 1, which no reader's namespace matches, so every reader treats the row as crossed. Off Linux, where namespaces do not exist, nothing changes. Claude-Session: https://claude.ai/code/session_01BpZwJcFxRtM9Wax4fHhDNC * fix(cli): an empty start token is not a match for a foreign-namespace reader (BUG-4090 r4) An unstamped row with no proc_start, read from a non-init or unreadable PID namespace, read alive whenever any process answered at its number there. That can honour a headless arm or disarm on an unrelated process. With nothing to match, such a reader now says unknown; the bare-probe residual stays for same-namespace and init readers. Claude-Session: https://claude.ai/code/session_01BpZwJcFxRtM9Wax4fHhDNC * test: namespace-correct session fixtures for runs outside the init pid namespace (BUG-4090) The Nix check runs go test in its build sandbox's PID namespace, where an unstamped registry row is judged by BUG-4090's foreign-reader rule. Three cmd/pad tests hand-wrote such rows and expected init-namespace verdicts. The two dead-pid fixtures are now stamped with the test's own pid_ns, so they mean "same namespace, dead pid" anywhere; the v1 legacy-row table assertion follows the test's namespace. internal/cli gains a seam test that drives the legacy row's verdict through init, non-init and unreadable readers on every runner. Claude-Session: https://claude.ai/code/session_01BpZwJcFxRtM9Wax4fHhDNC |
||
|
|
58c53d68eb |
feat(web): agent roster on /messages with Start chat, and a sign-out redirect (TASK-4044) (#2324)
* feat(web): ensureConversation client call, carried from the chat window work (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(web): chat roster, ensureDM and a reference-counted presence poll (TASK-4044) Ruling 018faf64: counter clamped at 0, DM keys filtered to kind dm, ensure fenced by identity. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(web): roster rows with Start chat on /messages, and a sign-out redirect (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(web): census entry for the roster each (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(web): roster keeps DMs by the server's 'direct' kind; presence owners are balanced (TASK-4044) The roster filtered on kind 'dm', which the server never writes (migration 158 allows 'direct' and 'group'), so against a real server every conversation dropped off the roster. Fixtures are corrected to 'direct', the Q3 row now uses a group conversation, and an e2e spec reads the server's real value. The chatInbox afterEach asserts no presence timer is left armed before one safety drain, so a leaked owner fails. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(web): roster ensure does not navigate after the page is left (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
36d6a27ca3 |
feat(web): agent presence in chat, with a presence dot and Try again on the thread placeholder (TASK-4044) (#2323)
* feat(web): carry the agent presence roll-up for chat (TASK-4044 2b-i) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(web): chatInbox presence poll with startPresence/stopPresence (TASK-4044 2b-i) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(web): PresenceDot component (TASK-4044 2b-i) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(web): presence dot and Try again on the chat thread placeholder (TASK-4044 2b-i) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
cd63dacae4 |
feat(web): Chat links in the user menu and You sheet, Back from chat, and a retry for a failed unread fetch (TASK-4044) (#2322)
* feat(web): chat Back link and last-workspace memory, parked for 2a-ii (TASK-4044) Restores what 8c6cb0280 moved out of 2a-i, byte-identical to bb37ef0e2: ChatBackLink, the lastWorkspace store, the workspace layout's write and their tests, and the two Back cases in messagesRoutes. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(web): retry a failed chat unread fetch after 15s (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(web): chat links in the sidebar, user menu and You sheet (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(web): pin that a success cancels an overlapped retry and an identity change cancels it for the new account (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(web): pin that a hint or resync near the retry's due time gives one fetch (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(web): a chat fetch in flight at stop() that then fails arms no retry (TASK-4044) A stop epoch, captured with the seq at the start of each fetch, gates the retry in the catch. Not a #started check: a never-started store driven by resync()/onHint() is the supported test seam and must still retry. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * refactor(web): no Chat link in the sidebar footer row (TASK-4044) With it, the desktop footer row overflowed its 235px (content 248px), the bell was clipped and Settings wrapped, failing the BUG-2844 guard spec. Chat stays in the TopBar user menu and the You sheet. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
a0d6437ae6 |
test(testtmp): decide a template dir's owner by a held flock, not its pid (BUG-4088) (#2321)
testtmp.Sweep judged a template directory's owner dead with kill(pid,0)
on the pid in its name. A pid only means something inside one PID
namespace, and /tmp is shared across namespaces. A test binary run under
bwrap --unshare-pid got ESRCH for every host pid and deleted live
storetest templates mid-run ("copy template db: no such file"). In the
other direction, a pid recorded inside a sandbox can name an unrelated
host process and keep a dead owner's directory forever.
MkdirTemp now builds the directory under a staging name no sweep
matches, takes a flock on owner.lock, and renames it into place. The
lock follows the inode across the rename, and the fd is O_CLOEXEC and
held for the life of the process. Sweep removes a directory only when
it can take that lock. A directory with no owner.lock (made before this
change) keeps the old pid rule. Windows is unchanged.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
|
||
|
|
b9b0aa784b |
test(cmd/pad): restore the root flag globals after tests that parse them (BUG-4086) (#2320)
workspaceFlag, formatFlag and urlFlag are bound to the root command's
persistent flags, so a test that parses --url/--format/--workspace
in-process writes process state that every later test reads. urlFlag
outranks PAD_URL, so a leaked URL sent a later test's CLI to an earlier
test's closed stub server. Default order hid it: building any root
re-binds the flags and zeroes the globals. Under go test -shuffle, 11 of
12 random seeds failed at
|
||
|
|
91b01fc4ee |
feat(web): /messages chat inbox list and placeholder thread (TASK-4044) (#2319)
* feat(web): /messages chat inbox list and placeholder thread (TASK-4044) The inbox list at /messages (URL-only, no link points at it yet) and a placeholder /messages/[n]. The root layout gives /messages no workspace shell; chatStore gains a `version` bumped after each successful unread fetch, and chatInbox reloads the list from one effect on it. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(web): pin the chat inbox identity fence on its own (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(web): census the chat conversation list's keyed each (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * docs(web): give chatStore.subscribe an accurate doc comment (TASK-4044) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * refactor(web): move the chat Back link to the next PR (TASK-4044) The 2a-i size ruling on TASK-4044 moves Back (ChatBackLink, the lastWorkspace store, the workspace layout's write into it and their tests) to 2a-ii, so entering and leaving chat ship together. /messages is reachable by URL only until then, and the browser's Back works. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(web): let a failed chat inbox list be retried (TASK-4044) The list reloads only when chatStore's unread fetch succeeds, so a failed list fetch beside a good unread fetch had no way back until the next focus or hint. The failure state now has a Try again button that calls chatInbox.load() (r1 hunter Low #1, ruled FIX). Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(web): say when a chat list refresh fails, and fence the placeholder by identity (TASK-4044) r2 rulings on TASK-4044: - A failed reload kept the last list silently. The notice and Try again now show whenever the list fetch failed, above a kept list too. - /messages/[n] fenced its answer by seq only. It now also drops an answer that outlives the identity it was asked for, like the inbox and the unread store. - The root layout test asserted any aria-live node as the toast surface. It now asserts ToastContainer's own markup, and its name no longer claims dialogs it does not check. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
17bfe78389 |
feat(web): relay chat inbox hints to a chat unread store (TASK-4044) (#2318)
* feat(web): chat unread types and api.chat.unread client (TASK-4044) ChatUnread / ChatUnreadResponse and api.chat.unread over GET /me/chat/unread, the only chat surface the unread store needs. Sliced by deletion only from the parked logic commit 98247acb8 (TASK-4044 packaging ruling S2); the other conversation and message types and methods ship with the PRs that first use them. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(web): relay chat inbox hints to a chat unread store (TASK-4044) The access stream now asks for chat_inbox=true and hands kind 'chat' notices to a new chat store instead of refetching workspaces. The store keeps the unread total and per-conversation counts from GET /me/chat/unread, treating each hint as a refetch trigger only: 400ms trailing debounce, one fetch in flight, one dirty follow-up, immediate fetch on resync, and an identity fence plus seq so a previous account's answer never lands. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(web): isolate chat hint subscribers; drop overclaiming chat comments (TASK-4044) r1 rulings: a subscriber that throws no longer aborts the hint fan-out (try/catch per call, logged), pinned by a test; and the api.chat docblock and the types header's wire-shape lines, which described surfaces this PR does not carry, are removed by deletion only. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * docs(web): drop the chat subscribe comment that names a surface this PR lacks (TASK-4044) r2 ruling: deletion only. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
25153bcda5 |
fix(decision): a failure the store refuses is still counted, and an empty message is never a supersede (BUG-4022) (#2317)
* fix(decision): a failure the store refuses is still counted, and an empty message is never a supersede (BUG-4022) FailDecisionJob's UPDATE carried the failure text. When the store refused it, the attempt went unrecorded: attempts stayed 0, the backoff never grew, the permanent drop never fired, and the job was re-run (a paid provider call) every lease, forever. It now fails open: one fallback UPDATE with the same WHERE and lease records the attempt with a fixed last_error, plus the SQLSTATE when it has the fixed five-character shape. Both errors are returned only if the fallback is refused too. A failure whose cause has no text (nil, or Error() == "") was read as a supersede, releasing the claim with no attempt and no last_error, so the job was re-run on every tick. releaseDecisionJob is split into releaseDecisionClaim (complete, release) and recordDecisionFailure (fail), and an empty cause records a fixed message. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * fix(decision): the fallback counts an attempt only if the first write did not (BUG-4022 r1 H1) Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV |
||
|
|
ccb4ed4a22 |
test(watch): run the identity-change monitor test on synctest's virtual clock (TASK-4067) (#2316)
* test(watch): run the identity-change monitor test on synctest's virtual clock (TASK-4067) TestMonitorChatIdentityChangeResetsMemory drove runWatchMonitor over a real loopback httptest.Server and paid real reconnect and identity backoffs (32s). It now runs in a synctest bubble over an in-memory net.Pipe listener, with http.DefaultTransport swapped for the bubble's life, and asserts exact virtual elapsed time per case. Every existing assertion is unchanged; the handler's 1ms ack poll is replaced by a channel so the times are exact. TestMonitorChatServerFloorHunterLocalDelivered pins both sides of the first-contact floor max(server floor, local delivered). Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * test(watch): state and enforce the DefaultTransport swap's no-parallel precondition (TASK-4067 r1) Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV |
||
|
|
6402eb3055 |
fix(mcp): stdio's item patch gives line_hint remote's rule through one shared helper (BUG-4082) (#2315)
* fix(mcp): stdio's item patch gives line_hint remote's rule through one shared helper (BUG-4082)
stdio's pad_item patch was passThrough, so edits reached the CLI unread:
its JSON decode into bodypatch.Edit.LineHint (an int) refused a string
hint that remote coerces ("5"), and took a JSON number of 2^53 or more,
which mcp-go's float64 decode may already have rounded, as a line number.
Remote has validated each edit's line_hint since BUG-4065, inline in
mapItemPatch.
That loop is now editsWithLineHints, called unchanged by mapItemPatch and
by a new stdio action, actionItemPatch. On stdio:
- a valid hint is forwarded as its int64;
- a refusal is validation_failed with remote's message, naming the edit
("(in edits[1])");
- null is absent, and a non-object edit keeps its path.
Neither transport modifies the caller's map.
Tests (lead ruling 86931478): one table, bug4082Cases, runs every input
through both transports and requires the same forwarded edits or the same
refusal (C1). Remote's BUG-4065 rows stay green unchanged. A row on each
transport pins the caller's map (C2). Red on main: stdio forwards "010",
1.5, 2^53, true and {} raw while every remote half passes.
ToolSurfaceVersion 0.105 -> 0.106 (C3), with the changelog in version.go
and docs/mcp.md; the README and the instructions.md heading follow. The
wire goldens follow in the next commit.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
* test(mcp): stdio's line_hint refusal checked on its own; actionItemPatch's input pinned directly (BUG-4082)
Mutants on
|
||
|
|
acc45070f6 |
feat(server): deliver chat inbox refs to the owner's own web stream (TASK-4044) (#2314)
A cookie-session stream that opts in with ?chat_inbox=true now receives kind "chat" notices addressed to its own user, so the global web chat view can refetch through /me/conversations instead of polling. - Pref: honoured only for a non-bearer, non-app session; ignored (not refused) on any bearer/PAT stream. No armed required. - Predicate: a notice with an empty target agent key delivers iff the stream has chat_inbox and the target user is the stream user. The existing agent clause is unchanged; the two cases are disjoint. - Publish: one human-target notice per human member, plus a self-notice for a human author (the store's recipient list leaves the author out), deduplicated on the delete path. Human notices are ref-only: excerpt, reply_to_seq and target agent key are cleared at publish time. Agent notices keep excerpt and reply_to_seq for the monitor. - docs/api.md: the param, the payload, the ignored-on-bearer rule, and the agent-path "never match" sentence scoped accordingly. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
729ae5f69c |
fix(mcp): stdio applies remote's number rule before a value becomes CLI text (BUG-4070) (#2313)
* fix(mcp): stdio applies remote's number rule before a value becomes CLI text (BUG-4070)
On stdio, a string for a "number" param reached the CLI verbatim. An int
flag parses it base 0 (pflag's ParseInt(s, 0, 64)), so "010" was 8, "0x10"
16, "1_000" 1000 and "+5" 5; pad_chat delete's positional seq parses base
10, so "010" was 10. Remote refuses all of them (decimalIntString,
BUG-4065). A JSON number was formatted and forwarded whatever its size,
where remote refuses one of magnitude 2^53 or more as possibly rounded
(wholeInt64, BUG-4061 /
|
||
|
|
ffecfc744b |
feat(chat): show chat delivered to an agent to the human at its terminal (TASK-4071) (#2312)
* feat(chat): tee monitor chat lines to a per-session chat-seen log (TASK-4071) pad watch --stream --for-session appends every chat line it prints to ~/.pad/chat-seen/<harness-pid>.log (0600 in a 0700 dir, O_NOFOLLOW), capped at 256 KiB with one .1 generation, so the human at the agent's terminal can see it. No log when the pid is pad watch's own; pruning a session record removes its log. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(chat): pin the chat-seen tee, perms, rotation, prune and C2 (TASK-4071) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(chat): refuse a FIFO that has a reader (TASK-4071 M13) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * feat(plugin): show chat delivered to the agent in a band and a toast (TASK-4071) The pad mod tails ~/.pad/chat-seen/<harness pid>.log from the session's first use of Pad, starting at the end of the file, and shows new lines in a band above the prompt (the last three, until the person's next prompt) and one toast per burst. Lines rotated away between polls are not shown. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * refactor(plugin): one rotation guard in the chat-seen reader (TASK-4071 P5) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(chat): report a log that is off on its first line, and say what prune and the monitor do with it (TASK-4071) The monitor prints nothing before it has something to deliver (DOC-2479), so the no-harness-pid line waits for the first chat line. The pad watch --stream --for-session and pad session prune help now describe the log. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(chat): r1 findings on the chat-seen log and reader (TASK-4071) - Refuse a symlinked chat-seen directory instead of chmodding its target. - Reopen a log that was removed under the writer. - Remove .log.1 before rotating: Rename does not replace on Windows. - Reader: drop control characters and direction overrides before they reach the terminal; clip by code points; one toast per burst on the relay's clock; a stop during start leaves nothing running; a failed first read no longer replays the log. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(plugin): r2 findings on the chat-seen reader (TASK-4071) - The first look skips what the log held at its stat, so a line written between that stat and the read still shows. - A stop during a poll no longer leaves the reader stuck after the next wake. - An unreadable moment (failed stat, a symlink) changes nothing, so it cannot cause a replay. - U+061C joins the stripped bidi controls. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * fix(plugin): r3 findings on the chat-seen reader (TASK-4071) - Start a log at zero only when the listing shows it absent; a first look that merely failed leaves the start to the first good read (R3-1). - Restore the byte-size rotation reset beside the character one: a log of multi-byte text can shrink in one while it grows in the other (R3-2). Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
0a32eff274 |
fix(scripts): install-refresh removes pad.pid only when it is provably not a live sibling's (BUG-4068) (#2311)
* fix(scripts): install-refresh removes pad.pid only when it is provably not a live sibling's (BUG-4068) The refresh removed <data dir>/pad.pid unconditionally after the install, whether or not it stopped anything and whoever the file named. A second server sharing the data dir lost its record, so `pad server stop` and `pad server status` could no longer find it. Now the file is removed when it names the pid this refresh stopped, or a pid that is provably dead (kill -0 fails and ps finds no such process, so EPERM is not read as dead). It is kept when it names any live process, when its liveness is unknown, and when it does not parse as a pid record (JSON or the legacy bare integer). A kept stale file is the safe failure: server stop refuses a record it cannot prove (BUG-2969). The BUG-4064 tests, which clear a dead-pid file with nothing stopped, stand unchanged. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * test(buildtools): pin install-refresh's pad.pid removal rule (BUG-4068) Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * fix(scripts,buildtools): pad.pid is read no more loosely than pad writes it, and only ESRCH is dead (BUG-4068 r1) r1 items, as ruled: 1, 3: pid_file_pid accepts only MarshalIndent's record (one "pid", at the two-space top-level indent) or a bare integer with surrounding whitespace, at most 10 digits. Duplicate or nested pid keys, compact JSON, "1 2" and huge values no longer parse, so the file is kept. 2: pid_liveness reads the builtin kill's own message under LC_ALL=C. Only "No such process" is dead; EPERM and anything else are unknown. ps is not asked, so a missing, busybox or hidepid-restricted ps cannot call a live sibling dead. 4: STOPPED_PID is recorded only when kill -TERM is delivered. 5: the decision is pid_file_removable, tested on its own, so the stopped-pid leg (a zombie or a quick reuse) has a row of its own. 6: the dead-pid row uses a reaped pid, not 999999. Also: the harness guard's testTempScope cuts the test name to 64 bytes, as Go's makeTempDir does. A top-level test name of 64 bytes or more was refused by its own temp dir; this unit's long test names found it. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * fix(scripts): pad.pid's reader checks the whole record's line shape and caps the pid (BUG-4068 r2) r2 items, as ruled: 1: pid_file_pid now checks the record's line SHAPE end to end, not only the pid line: first non-blank line exactly "{", last exactly "}", every line between a two-space-indent key line, one of them the pid. A "\r" is dropped only at a line end. This is still a shape check, not full JSON validation. The r1 commit's "accepts only MarshalIndent's record" said more than r1 did; this commit is what makes the shape claim true. 2: a pid above 4194304 (Linux's largest pid_max) does not parse, so it can never wrap as a pid_t. 3: pid_liveness states that ESRCH is per pid namespace. 5: the stopped-pid test comment is corrected: the e2e pre-server is a zombie until cleanup reaps it, so the e2e stopped-server rows reach that leg too. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * docs(scripts,buildtools): pid_file_pid's comments state the inner-line check exactly (BUG-4068 r3) Comment-only. pid_file_pid checks that each line between the braces STARTS like a two-space-indent quoted key; the rest of such a line is unchecked. The comments said "every line between a two-space-indent key line", and "read no more loosely than the SHAPE", which claimed more than the pattern checks (r3, ruled a decline: the code stays as it is after the close). Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV |
||
|
|
e09b11cc2e |
feat(attachments): in-app previews for text, Markdown and CSV/TSV (TASK-4074) (#2310)
* feat(attachments): add bounded CSV and TSV parser (TASK-4074) * feat(attachments): add capped table preview component (TASK-4074) * feat(attachments): enforce exact preview types and renderer byte caps (TASK-4074) * feat(attachments): mount table previews in bounded cell steps (TASK-4074) * feat(attachments): add raw markdown and text wrap controls (TASK-4074) * test(attachments): accommodate full-suite table coverage (TASK-4074) * fix(attachments): allow native table touch scrolling (TASK-4074 r1) * fix(attachments): focus and label the table scroll region (TASK-4074 r1) * fix(attachments): preserve preview choices on metadata fills (TASK-4074 r1) * fix(attachments): prove both navigation toggle resets (TASK-4074 r1) * fix(attachments): skip blank delimited-file lines (TASK-4074 r1) * fix(attachments): preserve literal quotes in delimited fields (TASK-4074 r1) * fix(attachments): preserve wide table arrow scrolling (TASK-4074 r2) * fix(attachments): preserve Wrap within its default class (TASK-4074 r2) * fix(attachments): truncate CSV cells by code point (TASK-4074 r2) * fix(attachments): cover table row resets on input changes (TASK-4074 r2) * test(e2e): the text card pans on both axes (TASK-4074) The card's touch-action is now pan-x pan-y, so a wide table or an unwrapped line can pan sideways; the IDEA-2712 spec still pinned pan-y. No viewer gesture collides: pan/pinch arm only over a decoded bitmap and nothing swipes between attachments. The stage-is-not-none leg stays. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * docs(attachments): the text card's touch-action comments say pan-x pan-y (TASK-4074) Comment-only: two CSS comments still named the pre-TASK-4074 pan-y. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
cba880ca17 |
fix(config): warn on a relative PAD_DATA_DIR / PAD_DB_PATH; anchor install-refresh's marker and config reads (BUG-4066 phase 1) (#2308)
* fix(config): warn once per process on a relative PAD_DATA_DIR / PAD_DB_PATH; install-refresh anchors its marker and config reads (BUG-4066 phase 1)
config.Load takes PAD_DATA_DIR verbatim, or PAD_DB_PATH's dirname, with no
filepath.Abs, so every process resolves a relative value against its own cwd:
a CLI and a server started in different directories silently use different
data (database, attachments, key). Phase 1 (lead ruling 4b4cb4ff) leaves the
resolution unchanged and makes it loud:
- One helper, envDataDir(), is the only reader of the two variables (Load and
userConfigPath both use it). A relative value warns ONCE per process,
naming the absolute path this process resolved, that a CLI and a server in
different directories will use different data, and that a future release
refuses it (TASK-4072). A literal '~' gets a hint. A CLI prints it to
stderr; `pad server start` logs it through slog; `pad watch --stream
--for-session` (the plugin monitor) suppresses it.
- install-refresh anchors the refresh marker and config_value the way 2a
anchors the log (${SERVER_CWD:-$PWD}), so the marker is written and
cleaned up in one place (it used to leak under the caller's cwd), and warns
before anything is read or stopped.
- docs/deployment.md: both variables must be absolute; relative is
deprecated and warns.
Tests: the warning rule (and Windows, GOOS-guarded), the marker and the
warning-before-stop end to end, config_value at function level, and
child-process tests for the monitor's silence and the server's slog line.
Harness guard (after this unit's draft test stopped the box's shared :7777
server twice): every install-refresh exec in internal/buildtools now passes
requireSafeScriptEnv first, which refuses an environment whose resolved port
is missing (the script would fall back to 7777) or is 7777, whose HOME or
data dir is the real user's or outside a test temp dir, or whose port answers
on a LAN address without this test's random stub token.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
* fix(config,mcp,scripts): the relative data-dir warning names the server's path and stays out of agent-facing errors; harden the test guard (BUG-4066 r1)
r1 rulings (c0ef54e8):
- install-refresh's warning now runs once SERVER_CWD is captured (still
before the marker and the stop) and names the anchored path
(${SERVER_CWD:-$PWD}/<dir>, a leading ./ stripped); when the caller's cwd
differs it also names the caller's reading. It used to name $PWD/<dir>,
which the server does not use.
- `pad mcp serve` checks the data dir once on its own stderr; the stdio
dispatcher's `pad` children get PAD_INTERNAL_NO_DATA_DIR_WARNING=1, so the
warning is not folded into a failed tool call's agent-facing error. The
variable is internal (code-documented only), so users cannot silence the
deprecation. Shell completion no longer prints the warning mid-Tab.
- Test guard: refuses a relative data dir with a config-sourced port (the
script reads that port from the server's cwd, which the guard cannot see);
compares symlink-resolved temp paths (macOS); asks for the stub token on
the LAN address that answered; compares 7777 numerically; and a source-scan
test fails if any install-refresh exec in the package is unguarded.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
* fix(config,mcp,scripts): the relative-dir warning names the data dir; completion and MCP fetchers stay quiet; harden the test guard (BUG-4066 r2)
- PAD_DB_PATH warning names the resolved data dir (Dir of the value), in Go and in install-refresh.sh; the script normalises . and .. in pure shell.
- Shell completion (__complete, __completeNoDesc) sets the silent sink in main.
- MCP resource and bootstrap fetchers exec with dispatchChildEnv; a population scan pins every pad exec in internal/mcp.
- Harness guard: numeric 7777 refusal before any dial (asserted by message), PAD_HOST must be empty or loopback, temp-dir check scoped to this test, hardened and self-tested source scan, marker-presence probe in the restarted stub, child timeout, sink reset.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
* fixup! fix(config,mcp,scripts): the relative-dir warning names the data dir; completion and MCP fetchers stay quiet; harden the test guard (BUG-4066 r2)
test(buildtools): the config-file 7777 row asserts the numeric refusal by message too (BUG-4066 r2 item 12, same class)
* test(buildtools,cmd): close the m15, m24 and m28 survivors (BUG-4066 r2)
m24: a scanner self-test case whose variable never runs in its function,
with a guard in a later function, so the function boundary is what flags it.
m28: insideTestTempDir refuses another test's directory, and admits a
subtest's, which t.TempDir names <Parent><sub...> with no separator.
m15: the --workspace completion func no longer sets its own sink. main()
silences every completion request, which made that line dead code.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
* fix(cli,buildtools): an auto-started server does not inherit the warning silencer; the guard reads the config port like the script (BUG-4066 r3)
Item 2: an MCP tool-call child carries PAD_INTERNAL_NO_DATA_DIR_WARNING=1
and is often the process that auto-starts the server, so that long-lived
server never logged the relative data-dir warning. serverStartCommand drops
that one variable from the spawn env, and keeps every other one. It is the
only `server start` spawn in internal/cli; a source test pins both facts.
Item 7: the harness guard took the leading digits of config.toml's port, so
"8080abc" read as 8080 where install-refresh rejects it and falls back to
7777. It now reads the first port line the way config_value does, and a
non-numeric value is refused as unreadable.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
* fix(scripts): install-refresh's relative-dir warning is one line; the completion tests run main() (BUG-4066 r4)
Item 1: when the server's cwd differs from the caller's, the warning printed
a second "Warning:" line naming the shell's-directory reading, and a third
for a leading ~. Phase 1's contract is one warning per process, as
config.Load prints it, so both now ride in the one line. The test asserts
both paths sit in that line and that it prints once.
Item 6: the completion child cases called silenceWarningsForCompletion
themselves, so deleting main()'s call passed every test. They now set
os.Args and run main().
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
|
||
|
|
bc2a32895e |
feat(chat): monitor reply quotes and a session-start DM ensure (TASK-4043) (#2309)
* feat(chat): render monitor reply references and excerpts (TASK-4043) * feat(chat): ensure owner-agent DM once per monitor process (TASK-4043) * docs(chat): describe monitor quotes and DM startup (TASK-4043) * test(chat): accept startup DM ensure in recovery fixture (TASK-4043) * fix(chat): wait a default when a 429 has no Retry-After (TASK-4043 r1) * fix(chat): bound Retry-After before it can overflow the wait (TASK-4043 r2) A Retry-After too large for a time.Duration (>= 9223372037s) wrapped negative and made the ensure retry hot. Seconds are now bounded before the multiply; zero, negative, absent and overflowing values all wait the 120s default, and the largest value that fits is honoured. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg * test(chat): keep the Retry-After table portable and hermetic (TASK-4043 r3) The overflow rows are int64 and skip where int cannot hold them; the rows run with a temp HOME. Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
82f81ee828 |
fix(mcp): refuse a non-number for a numeric parameter instead of reading it as absent (BUG-4065) (#2306)
* fix(mcp): refuse a non-number for a numeric parameter instead of reading it as absent (BUG-4065) numericInput read any value outside float64, int, int64 and json.Number as absent, so a string, a bool or an object for limit, offset or days was silently replaced by the default. itemExpectedSeqParam had its own switch with the same fall-through, so a string expected_seq sent the content write out UNGUARDED. Now only null is absent: - A string is admitted only as a plain decimal integer (an optional '-' and digits), parsed exactly and bounded like a JSON number (|x| < 2^53), so "10" is 10 and "123" is a guarded write. That matches what stdio's CLI flags and the buildQuery doors already accept (lead ruling af4bff40, Q1). Every other string, and every other type, is refused with validation_failed naming the parameter. - Every Go integer type is admitted exactly; an unsigned value above MaxInt64 is refused. - itemExpectedSeqParam, pr_number and chatSequence share that one rule (pr_number's bound becomes the exclusive 2^53; chatSequence's float64 gets the 2^53 bound and an exact json.Number). Their messages and hints are unchanged. - item patch validates expected_seq and edits[].line_hint locally, and the collection and role update doors validate sort_order, so each answers validation_failed naming the field rather than the server decode's 400. - buildQuery routes the native integers through the same check; strings are still forwarded verbatim. - The item-list resource's limit flag is refused when malformed, not defaulted. ToolSurfaceVersion 0.103 -> 0.104, with the changelog in version.go and docs/mcp.md; the README, instructions.md heading and wire goldens follow. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * fix(mcp): numeric query keys and leading-zero strings are refused, not defaulted or re-read (BUG-4065 r1) r1 found two Mediums (lead ruling 5a06782e): - The buildQuery doors forwarded a bool, an object or a non-integer string for a numeric key (item backlinks, item search, item context's budget, project activity, project standup and changelog). The server Atoi'd it and silently kept its default. Each route now names its numeric query keys (routeSpec.numericQuery, and the explicit lists at the direct buildQuery callers), and those keys go through numericInput. TestBUG4065_NoRouteForwards ANonNumberForANumberParam walks every remote route against every parameter its tool declares "number", so a new numeric key cannot be left off. - "010" was 10 here and 8 on stdio (pflag parses base 0). A plain decimal string is now ^-?(0|[1-9][0-9]*)$, so a leading zero is refused. The stdio side is BUG-4070. Also: an end-to-end row through the pad_item tool handler, value rows at the chat doors, an array value at every door, absent-means-default, the resource flag's negative row, stale expected_seq comments, and the v0.104 changelog corrected in version.go and docs/mcp.md. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * test(mcp): the numeric-param population walk cannot pass vacuously; uintptr admitted (BUG-4065 r2) r2 found the r1 population walk could pass on broken code (lead ruling b9025d7c): it looked for the raw bad value under the catalog name only, so a rename, a path segment or a value dropped as absent was invisible, and its "x"-filled inputs tripped unrelated validation, so most routes refused before reading the number. With a route's numericQuery dropped, the walk alone stayed green. The walk now uses a differential oracle from a curated valid base: each (route, number param) is dispatched with the param absent, as "7", and as a bad value. If "7" changes what is forwarded, the route reads the param and a bad value must be refused naming it with no write; otherwise a bad value must forward exactly what the absent case does. An inconclusive case fails. The derived read-set is pinned in bug4065ReadSet, so a route that stops reading a param altogether cannot be reclassified and go green. Also: uintptr is admitted like the other unsigned types; an empty string and false are named as refused in the v0.104 changelog; rows for a missing key, a null expected_seq (still unguarded), "" and a 20-digit string at every door. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * test(mcp): false, the catalog limit doors, and the chat null claim (BUG-4065 r3) r3 rulings (9bd7fa2d), all Low: - false is fed to the unit table, the door table and the population walk; the changelog says it is refused, and nothing pinned it. - pad_item.list and pad_item.history read limit through intParam before the dispatcher; TestBUG4065_CatalogLimitNonNumbers refuses "abc", true, false, an object, "010" and "" there naming limit, and uses "7". - The v0.104 changelog says pad_chat's numeric params still refuse a present null, as before, with the same message; "only null is absent" read as if they did not. - A misnamed row; the refused pr_number rows assert no request went out. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * docs(mcp): reflow a joined comment; state the population walk's limit (BUG-4065 r4) Comment-only (lead ruling 86de95f1): my r1 edit joined two comment lines in dispatch_http_advanced.go into one; and the walk's doc comment now says conditional reads are outside it, with edits[].line_hint covered by the door table. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV |
||
|
|
8873978b1d |
test: close surviving-mutant gaps from the #2284-#2290 audits (TASK-4051) (#2307)
* test(push): verify configured gateway wiring (TASK-4051) * test(push): verify server shutdown stops dispatch (TASK-4051) * test(push): cover committed updates with lost row counts (TASK-4051) * test(notifications): cover owner and actor lookup errors (TASK-4051) * test(catch-up): cover caller scope and ceiling label (TASK-4051) * test(push): fail shutdown regression without blocking cleanup (TASK-4051 r1) * test(push): drive retry settlement across record chunks (TASK-4051 r1) * test(push): pin a gateway batch inside one record chunk (TASK-4051 r1 D2) Claude-Session: https://claude.ai/code/session_017ofcsFmgnoVDPxeHwuxCjg |
||
|
|
3e7f9e18cf |
fix(scripts): install-refresh restarts the server logging to cfg.LogFile() (BUG-4064) (#2304)
* fix(scripts): install-refresh restarts the server logging to cfg.LogFile() (BUG-4064) The restart appended to $HOME/.pad/server.log, a file nothing else names: `pad server info`, the daemonized start and every "Check <log>" message use cfg.LogFile(), <data dir>/logs/server.log. Anyone following server info read a log days stale. The old line also ignored PAD_DATA_DIR. The log path now comes from the script's data_dir(), which mirrors config.Load, and its directory is created before anything is stopped. The refresh names the log in its report and on a failed probe. An existing ~/.pad/server.log is left untouched and named in a one-line note. Tests: both restart branches, with and without PAD_DATA_DIR, find the restarted process's own stdout line in the configured file and nothing at the old path; the old file survives byte-for-byte. stubEnv now pins PAD_DATA_DIR and clears PAD_DB_PATH so the developer's environment cannot move the data dir out of the test HOME. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * fix(scripts): install-refresh proves the log writable before the stop; pid file in the data dir (BUG-4064 r1) - The log is opened for append (create or append) in a new section 2a, before the marker and the stop. An unwritable log used to stop the old server and then fail the restart's redirect, leaving nothing on the port. Now the refresh refuses with the path and the running server untouched. - The stale pid file is cleared at "$(data_dir)/pad.pid", where `pad server start` claims cfg.PIDFile() (cmd/pad/cmd_server.go, config.go), not at $HOME/.pad/pad.pid regardless of PAD_DATA_DIR. - The "mkdir ~/.pad before the redirect" block, which this change made untrue, is rewritten. A PAD_DATA_DIR refresh no longer creates a stray ~/.pad. Tests: a refusal on an unwritable log (a directory in its place) leaves the pre-server answering as itself; a stale pad.pid in PAD_DATA_DIR is gone after the refresh. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV * fix(scripts): install-refresh anchors a relative data dir to the server's cwd for the log and pid file (BUG-4064 r2) config.Load takes PAD_DATA_DIR verbatim, so the restarted server resolves a relative data dir against its own cwd. The writability proof and the pid-file removal now use that same anchored path (SERVER_CWD, else $PWD), so the file checked before the stop is the file opened after it. Also corrects the "root-owned" comment, and adds TestInstallRefresh_AnchorsARelativeDataDirToTheServersCwd. Mutation run (~/.cache/rook/4064-mut4.log, plus 4064-mut4-m8.log): baseline passes, and m1-m11 are all DETECTED. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV |
||
|
|
ba981cb4f2 |
fix(mcp): refuse a numeric parameter that is not a whole int64 instead of changing it (BUG-4061) (#2303)
* fix(mcp): refuse a numeric parameter that is not a whole int64 instead of changing it (BUG-4061)
numericInput converted a float64 with int64(x), so limit 1.5 read as 1, -2.7
as -2, and a value outside int64 as an implementation-defined number. A
json.Number that did not parse read as absent, so the parameter was dropped.
- numericInput returns an error for a fraction, a value outside int64 (2^63
is out), and an unparseable json.Number. intParam names the parameter.
- All 10 caller-input sites refuse with validation_failed: the route mappers,
item history, attachment list, and the pad_item.list and pad_item.history
catalog actions. pad_item.list's documented truncation of a fractional
limit to the default is reversed by ruling; zero still means the default.
- actionItemGet's read of the server's own seq stays lenient (pinned).
- itemExpectedSeqParam refuses a float64 outside int64 and gets the same
json.Number refusal, rather than dropping the token (an unguarded write).
- Comments note the unreachable float32 paths in formatNumber and
chatSequence.
- ToolSurfaceVersion 0.103: the parameters are declared "number", so the
refused inputs were valid under the advertised shape. Goldens move only by
the version string.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
* fix(mcp): buildQuery refuses a number that is not a whole int64 (BUG-4061 r1)
Round 1 (Codex Medium, and the hunter's standup/changelog question): the
query-string doors built by buildQuery (item backlinks, item context budget,
project activity, item search, project standup and changelog) forwarded
"1.5", and the server parsers read it as absent and used their default.
buildQuery now refuses with the key named, and its four callers return
validation_failed (lead ruling on Q2, option (a)).
- TestBUG4056BuildQueryNumbersUnchanged: the 2.5 and 1e19 rows are now
refusals, superseded by that ruling. The notation rows stand, and
json.Number "1e6" forwards as "1000000".
- jsonNumberInt64: a whole json.Number in exponent form ("1e6") is
accepted. Int64() alone refused it. Used by numericInput and
itemExpectedSeqParam.
- The v0.103 changelog door list is corrected. The instructions.md and
README version headings follow 0.103, which
|
||
|
|
d3f87d205a |
feat(chat): delivery, framing and the accept-instructions opt-in (TASK-4041) (#2305)
* WIP TASK-4041: add ref-only chat delivery and explicit acknowledgements * WIP TASK-4041: implement local consent and fetched monitor delivery * WIP TASK-4041: document chat delivery and session instruction consent * WIP TASK-4041: rename chat arm files (_arm.go is a GOARCH build constraint) * test(chat): fix delivery fixture and strengthen consent guards * test(chat): pin replay publication guard after mutation audit * test(chat): make review guards detect their named regressions * docs(chat): clarify bounded excerpts and defer unwired command guidance * test(chat): cover empty agent pairs and the Redis envelope * fix(chat): seed gap fills and acknowledge contiguous output * test(chat): restore gap regressions for amended ack rule * fix(chat): advance contiguous delivery across own rows and tombstones * test(chat): cover final tombstones, ack retries, and sync recovery * fix(chat): acknowledge final contiguous tombstone events * fix(chat): retry contiguous delivery ack on duplicate events * fix(chat): reseed delivery baselines on sync required * fix(chat): wire session consent commands and reuse chat client Pin arm/disarm execution through the real root tree and restore the deferred skill and monitor guidance. Wiring adds both local consent commands to the cmdhelp Document supplied to MCP. Preserve TestChatArmHasNoMCPPath's failure for the requested ruling; do not change MCP goldens or versions. * test(mcp): pin chat direction dispatch across transports * docs(cli): add chat direction command examples * test(chat): expose delivery memory loss and unsafe acknowledgements * fix(chat): retain process delivery memory independently of fill cursors * docs(chat): describe local delivery memory and failed-summary recovery * test(chat): pin both monitor adapter fetch limits * docs(api): state chat owner-message framing in the present tense (TASK-4041) The TASK-4039 provenance caveat described TASK-4041 behaviour as future work. It is implemented now: say what is true and point at the framing rules under GET /api/v1/events/stream. Claude-Session: https://claude.ai/code/session_01Cc68dt7DDR28fp7cxKYkbt * test(chat): expose failed resync, late retraction and identity collisions * test(chat): keep push delivery when monitor identity is unavailable * fix(chat): preserve closed recovery and retraction ownership per identity * docs(chat): clarify recovery, page cap and identity memory limits * test(chat): recover after a transient monitor identity lookup failure * fix(chat): retry transient identity failures on chat references * test(chat): require deferral until a safe server summary * fix(chat): defer events until a server delivery floor is available * test(chat): pin per-conversation server floors and caller boundaries * feat(chat): expose caller delivery floor on conversation GET * fix(chat): fetch per-conversation floors before monitor delivery * test(chat): require bounded synchronous first-contact delivery and backoff * fix(chat): fetch first-contact floors synchronously with bounded backoff * test(chat): remove redundant break from synchronous stream fixture * test(chat): cover stream deadlines, retry windows and cookie agent isolation * fix(chat): bound stream HTTP calls and back off failed conversations * docs(chat): clarify direction consent and stream delivery limits * test(chat): preserve independent dedupe eviction and retry clocks * fix(chat): require bearer authentication for stream agent addressing * test(chat): cover r10 tombstone, summary and arm-generation regressions * fix(chat): fill tombstone gaps and back off unread summaries * fix(chat): bind direction consent to a random arm generation * test: pin r11 retractions and session direction cleanup * fix: retract owed chat deletions before network backoff * fix: report session action when direction removal fails * docs: clarify chat direction rearming and connect state * test(chat): bound HTTP deadline failures and cover unresolved retractions * docs(chat): clarify immediate retractions and session re-arm direction loss |
||
|
|
8f1221cf1d |
test(chat): name the 65-rune ensure test for the gate it pins (TASK-4059) (#2302)
TestChat_Ensure_65RuneBodyRefusedWithoutCreation read as pinning the 64-rune agent_key bound, but a 65-rune human body key is refused first by the normalizeAgentName(key) != key equality gate, and the handler's rune-count case is shadowed (BUG-4055). Reverting the bound left it green. It also did not pin the equality gate: with that gate disabled, the shadowed rune case refused the key with the same 400 validation_error and the test still passed. It now asserts the equality gate's own message, and it is renamed and commented for what it pins. The bound stays pinned by the store table and the 64-rune non-ASCII round trip. Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV |
||
|
|
b1dce0eddb |
feat(server): log every unguarded content write the PLAN-3555 counter counts (TASK-4062) (#2301)
pad_content_writes_unguarded_total and pad_collab_append_failures_total are
in-process counters with no scraper, zeroed by every restart, so the week
PLAN-3555 phase 2 waits on cannot be read from them. Log each event instead,
so the count survives restarts in the server log.
- handlers_items.go: one slog.Info("content write unguarded") per Inc(),
outside the metrics guard, carrying door, kind, item ref and id, workspace
id, actor kind and actor user id. Never the body, the title or a field value.
- Collab needs no new line: "collab: append op-log" already sits in the same
branch as the counter. Its test now pins one line per counted failure.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
|
||
|
|
0ed34ea9f7 |
fix(mcp): integral numbers keep decimal notation on both transports (BUG-4056) (#2300)
* fix(mcp): preserve decimal numeric arguments (BUG-4056) Share numeric formatting across CLI dispatch and HTTP queries, and validate remote chat sequences directly with int64 bounds. Preserve the existing tool surface and wire goldens. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * fix(mcp): numeric array elements, playbook tokens and buildQuery's default share formatNumber (BUG-4056, r1) Lead ruling on r1 (tier A from here): toStringSlice spelled each JSON number element of an array with fmt.Sprint, so a repeatable flag or variadic positional given 1000000 got '1e+06'; playbook raw_args tokens used %v, so a string- or ref-typed argument would bind '1e+06'; buildQuery's default branch kept fmt.Sprint for float32. All three now use formatNumber. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd |
||
|
|
e8e2a698b8 |
test(chat): sibling-DM coverage for inbox, unread and ensure (TASK-4060) (#2299)
* test(chat): sibling-DM coverage for inbox, unread and ensure (TASK-4060) From the lead's post-merge rule-3a round on #2296. TestChat_AgentCannotReachSiblingAgentDM now also covers the endpoints that could reveal bravo's DM without naming it: alpha's inbox and unread summary carry no row for it and their totals exclude it, with bravo's DM holding unread rows. Alpha ensuring agent_key "bravo" is refused 400 with no DM bucket allocated and no conversation created, which is where the DM bucket assertion can actually move. The delete case targets bravo's own message so an author check cannot mask a leak, and the header comment says which layer catches a single-layer leak and that the bucket claim is allocation only. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * test(chat): make the ensure case's bucket and row checks able to fail (TASK-4060, r1) Lead ruling on r1: the ensure case's DM-bucket and row-count assertions were vacuous. The fixture's API ensures had already allocated the owner's ChatDM bucket, and "bravo" already had a DM, so a charge or a create before the refusal could not show. Both DMs are now seeded through the store, and alpha is also refused naming "charlie", which has no DM yet. The seed comment now says which unread row a leak would expose. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * test(chat): comments state what the sibling-DM cases prove (TASK-4060, r2) Lead ruling on r2, comment-only: the delete case's rationale now says what bravo's own message catches (alpha resolved as bravo would delete it), and the header describes the single-layer leak behaviour instead of citing an off-repo mutant id. The header also names the second ensure. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd |
||
|
|
6e37c69934 |
test(cli): arm-state tests own the session pid env (BUG-4025) (#2298)
* test(cli): arm-state tests own the session pid env (BUG-4025, BUG-4057) armStateTestEnv reset HOME and the messaging socket but not PAD_SESSION_PID or CLAUDE_PID, which CaptureSessionOwner prefers over this process. A gate inherits them from the harness that started it, and when that session is relaunched mid-run the dead pid made TestArmState_HeadlessLivePid read disarmed. The helper now clears both. TestArmStateTestEnv_IgnoresAmbientSessionPID plants a dead pid first, so the guard holds in CI too, where CLAUDE_PID is never set. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * test(cli): put HeadlessLivePid's doc comment back on it (BUG-4025, r1) Lead ruling on r1: the new test had been inserted between TestArmState_HeadlessLivePid and its doc comment. Comment-only. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd |
||
|
|
5308535c02 |
fix(chat): one 64-rune bound for agent keys (BUG-4055) (#2297)
* fix(chat): share the 64-rune agent key bound (BUG-4055) Use the attribution and presence name limit for chat headers and stored keys. Refuse overlong explicit body identifiers while normalizing self-declared headers, and cover Unicode round-trips and presence agreement. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * test(chat): an agent names itself by the normalised key on ensure and unread (BUG-4055, r1) Lead ruling on r1: the agent-caller equality checks (ensure body agent_key, unread ?agent=) were untested now that headers truncate. A 70-rune header's agent must pass the 64-rune key and be refused the raw name. The shadowed rune-count case in the ensure switch now says it is shadowed and by which gate. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd |
||
|
|
811154a4c7 |
test(chat): pin per-agent separation on every conversation endpoint (TASK-4054) (#2296)
* test(chat): pin per-agent separation on every conversation endpoint (TASK-4054) One owner, two agents: alpha gets 404 on get, list, send, delete and mark-read for the owner's DM with bravo, with a body identical to a missing conversation's, no send or DM bucket allocated, and no change to bravo's conversation (last_seq, member markers, tombstones). Each endpoint is asserted on its own terms, so the director's mutant (the agent key dropped from resolveChatConversation) no longer depends on the send bucket's side effect to die. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * test(chat): baseline side effects per case so each leak names its own endpoint (TASK-4054) The first mutant run showed one endpoint's leak (send's bucket) reported again by the later delete and read cases, because the state and bucket baselines were taken once before the loop. Each case now takes its own baseline and checks side effects before issuing the missing-number reference request. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * test(chat): check rows.Err in the snapshot and the reference 404's status (TASK-4054, r1) Lead ruling on r1: an iteration error must not truncate the DM state snapshot silently, and the missing-conversation reference response must itself be a 404 before its body serves as the oracle. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd |
||
|
|
51b0bb4de9 |
feat(cli,mcp): pad chat CLI and pad_chat MCP tool (TASK-4042) (#2295)
* feat(cli): add global pad chat commands (TASK-4042) Add conversation targeting, inbox and message commands with caller-owned idempotency and precise read markers. Preserve API responses for JSON output and use cwd workspace links for reference context. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * fix(cli): add chat examples and isolate command tests (TASK-4042) Satisfy the CLI help-tree example contract and restore package-level flags after command regression tests. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * feat(mcp): expose global pad_chat on both transports (TASK-4042) Add six chat actions with shared target parsing, uniform authenticated dispatch, precise read markers and structured unnamed-agent errors. Exclude agent chat from ChatGPT and pin the direction guard. Bump the surface to 0.102, update documentation and goldens, and shorten duplicated prompt prose within the fixed budgets. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * test(mcp): label global chat scope judgments (TASK-4042) Mark the membership-based global-chat exemption reasons explicitly, as required by the allowlist coverage guard. Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * fix(chat): preserve unread gaps and successful outcomes (TASK-4042, seat r0) Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * fix(chat): restore agent rules within prompt budgets (TASK-4042, seat r0) Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * fix(chat): keep scope fixture contiguous with read marker (TASK-4042, seat r0) Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * fix(chat): guard read boundaries and direction inputs (TASK-4042, r1) Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd * fix(chat): validate MCP bodies and report mark outcomes (TASK-4042, r1) Claude-Session: https://claude.ai/code/session_01KYZ6vrKNro953q9kfdSGFd |
||
|
|
f8756a0b5b |
feat(server,cli,plugin): agent presence — agent name, worker id and busy/idle activity (TASK-4040) (#2293)
* test: pin agent presence and worker-scoped session activity * feat: carry agent presence and track activity by user and worker * docs: describe worker presence and session activity states * fix: include hook activity examples in CLI help * fix(cli): keep session activity hooks silent on failures * fix(server): tolerate activity read failures in session lists * docs: place activity keys after presence registry details * fix(plugin): wire silent Claude Code activity hooks * docs: explain session busy-state hook limits * fix: bound the entire session activity command * fix: batch session activity reads across workers * docs: describe detached activity reports and timestamp ordering * fix: order worker activity atomically by client timestamp * fix: detach hook activity reports with captured timestamps * test: reproduce detached activity losing registered identity * fix: capture activity identity before detaching the reporter * test: isolate activity registry home on Windows * docs: clarify activity ordering and hook failure behavior * fix: bound activity reordering to ten seconds * fix: replace activity records with corrupt client timestamps * test: prove activity parent exits while child response is held * fix(session): derive activity reorder window from report bound * fix: replace non-string Redis session activity records * test: cover cleared and expired session activity ordering * test: cover older timestamp after legacy activity * test: cover corrupt activity nanos and malformed JSON * test: verify activity child deadline and process isolation * docs: clarify read-only tokens cannot report hook activity * test: strengthen session activity liveness and timing guarantees * test: pin malformed null session activity timestamps * test: harden session activity detach regression checks * test: skip activity process-group check when ps is unavailable * test: tie activity parent-exit bounds to the HTTP timeout * test: bound activity liveness checks by the child HTTP deadline * test: require ps for process-group checks in Go CI |