mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 22:10:25 +00:00
main
2796 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
1c6c9740ab |
feat(server): chat HTTP API and access model (TASK-4039) (#2292)
* feat(server): chat API core: conversations, messages, read marker, unread, limits (TASK-4039, part A) Add membership-gated chat HTTP endpoints, credential-derived authorship, private wire DTOs, replay and read markers, and per-agent/user limits. Document the access and provenance model and pin it with handler guards. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * feat(server): chat read-time visibility for context workspace and item refs (TASK-4039, part B) Authorize context workspaces on send and at read time, omitting hidden contexts and projecting item refs through the caller's current access. Keep unknown and denied refs indistinguishable, with unresolved overflow past 50 distinct refs per response. Pin the wire behavior and document it. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * fix(chat): a retry replays after context loss; sentinel for context refusal; source, ref cap and empty body (TASK-4039, r1) F1: Add a read-only replay lookup scoped to the author's current conversation membership, and replay identical HTTP retries after context access loss with the same ChatSend charge as normal replays. F2: Use a context refusal sentinel wrapping ErrInvalidConversationInput while preserving the store error text and HTTP response body. F3: Derive chat source from the actor's bearer signal, preserving in-process remote MCP attribution; assert the router rejects stray Basic authorization. F4: Count only distinct refs with a workspace toward the 50-ref lookup cap. F5: Accept empty conversation-ensure bodies for agents while humans still require agent_key. Document the lookup cap and remaining-token header. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * fix(chat): refs capped at 200 with refs_truncated; ensure charges only a create (TASK-4039, r2) L1: Cap deduplicated response refs at 200, retain the 50-lookup limit, report refs_truncated on list/send/replay, and stop extraction after a drop. Document plain-text fallback and cover 6,000 refs, duplicates, and false flags. L2: Validate agent keys and check the owner's inbox before charging ChatDM. Reuse is free; only creation attempts spend a token. Document the concurrent create race and cover 40 reuses, invalid keys, and the 30/hour creation cap. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * fix(chat): a human's agent_key must already be a normalised agent name (TASK-4039, r3) Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc |
||
|
|
59bab6aff8 |
feat(store): chat conversations, members and messages with per-conversation seq (TASK-4038) (#2291)
* feat(store): chat conversations, members and messages with per-conversation seq (TASK-4038, run A) Migrations 158 / pg126 (tables), generated NUL triggers 159, NUL and migrated-refusal registration, models, EnsureAgentDM, Get*, Send (seq from conversations.last_seq RETURNING, op_key replay, reply quote) and ListConversationMessages, with the explicit conversations delete in eraseUserTx. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * feat(store): chat delete, read markers, unread and inbox list; claim removes chats (TASK-4038, run B) Add authorized, idempotent message tombstones, atomic clamped read and delivery markers, pair-based unread summaries, and inbox listings with batched previews. Clear conversations and memberships during account claims so a new holder cannot inherit the previous holder's chats. Cover authorization, tombstone replays, markers, unread counts, inbox ordering, account deletion and claims, and workspace purge context retention with regression tests. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * fix(store): chat send refuses bad input and a dead context workspace, locks the workspace first (TASK-4038, r1) F1: Reject NUL and invalid UTF-8 in body, source and op_key before opening a transaction. Reject non-positive delete sequences as invalid conversation input. F2: Validate context workspace ID text and its 128-byte cap before the transaction. Inside the transaction, require a live workspace and take FOR KEY SHARE on Postgres before updating the conversation. This matches the workspace-then-chat lock order of PurgeWorkspaceData and avoids a cycle between the message INSERT's FK lock and ON DELETE SET NULL. F3: Raise the dialect-neutral substr quote prefix from 400 to 65536 characters. Every valid body is at most 64 KiB of UTF-8, hence at most 65536 characters; the SQL transfer stays bounded while QuoteExcerpt can find the first non-empty line and keep its result within 100 runes. This retains Unicode whitespace handling and collapse of arbitrarily long whitespace runs within that line, which trimming just ASCII leading whitespace before taking a short prefix would not fully preserve. Add the four requested regression tests, observed failing before the fix, including body-cap, Unicode whitespace and rune-cap quote cases. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * fix(store): chat timestamps are fixed-width; page carries read_seq (TASK-4038, r2) G1: Use chat-local fixed-width UTC timestamps for every chat write; cover trailing-zero ordering, parseTime round trips, and raw column widths. G2: Cover self-sequence and far-future reply rejection without spending a seq. G3: Prove inbox previews select the highest seq despite older timestamps. G4: Rename actor membership coverage and document EnsureAgentDM's caller duty. G5: Include the actor's read_seq on message pages, verify marker tracking, and document directional paging, recipients, and replay notification rules. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * fix(store): chat context workspace must be the author's; actor input validated on every method (TASK-4038, r3) H1: Require the author's workspace_members row for context workspaces, return identical not-found errors for unavailable contexts, and retain the workspace row lock before last_seq updates. Add non-member and error-equality regression coverage and explicit workspace memberships in the context and purge fixtures. H2: Validate actor user ids and agent keys before SQL in every actor-taking conversation method, reuse the helper for send authors, and cover malformed identities across all read, marker, delete, inbox, and send paths. H3: Document why store-generated owner UUIDs make AgentDMKey unambiguous even when agent keys contain colons. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * fix(store): EnsureAgentDM validates its owner id like every actor method (TASK-4038, r4) A NUL or invalid-UTF-8 owner id reached SQL and surfaced as a driver or foreign-key error. It is now refused as ErrInvalidConversationInput by the same validateConversationActor every actor-taking method runs. TestEnsureAgentDMValidatesOwner went red under the old empty-only check. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * fix(store): a chat op_key retry replays even after the context workspace is lost (TASK-4038, r5) The context workspace check ran before the op_key lookup, so retries of committed sends failed when the workspace was soft-deleted or the author lost workspace membership. When that check finds no row and an op_key is present, look up the original message in the same transaction, gated by current conversation membership and the exact author pair. Reuse the existing message loader for tombstones and quote previews, return Replayed without recipients, and roll back the read-only attempt. Missing or unauthorized replays retain the exact context workspace error. Keep the normal path and workspace-before-conversation row lock order unchanged; the fallback lookup takes no row lock. Add regression coverage for both context-loss cases, unknown keys, other authors, outsiders, removed conversation members, tombstones, and quotes. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc * fix(store): a chat send leaves read and delivered markers alone; op_key lookups use their index; malformed ids are not-found (TASK-4038, r6) M1 reverses the earlier "a send advances the sender's own marker" decision. Read and delivered markers are high-water marks: advancing them on an agent's reply acknowledges earlier human messages the agent may never have fetched. Leave both markers alone on sends; clients explicitly mark the sequence they actually displayed with MarkConversationRead, and delivery advances only via MarkConversationDelivered. Own messages already do not contribute to unread counts. Update send documentation and preserve existing marker assertions by setting up explicit acknowledgements in paging, unread and inbox tests. I1 shares the exact op_key lookup SQL between both replay paths and their SQLite query-plan test. Add the literal nonempty op_key predicate so the partial unique index can serve normal replays and context-loss replays. L2 rejects empty, NUL, invalid-UTF-8 and over-128-byte conversation ids as ErrConversationNotFound before SQL in all six id-based methods. Exercise these cases against open and closed databases to preserve 404 semantics without leaking SQL driver errors. Claude-Session: https://claude.ai/code/session_01LL3BV3H4zot6QrMQMsVfVc |
||
|
|
fdbba1a31e |
feat(cli,mcp): pad project catch-up and pad_project action=catch-up, read-only (TASK-4035) (#2290)
* feat(cli,mcp): pad project catch-up and pad_project action=catch-up, read-only (TASK-4035)
Read-only wrappers over GET /workspaces/{ws}/catch-up on the CLI, local and
remote MCP, and the ChatGPT surface (catch_up). Mark caught up (PUT /me/seen)
stays web-only (ruling 380aa793): no verb, flag or client method moves the
cursor. ToolSurfaceVersion 0.100 -> 0.101, additive. Existing tool
descriptions were reworded to fewer bytes (no rule, caveat, refusal or ref
dropped) so tools/list keeps 901 B of headroom under an unchanged ceiling.
Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5
* test(cli,mcp): ChatGPT catch_up in the wire golden and the leak sweep (TASK-4035)
Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5
* fix(cli,mcp): catch-up json skips the typed decode; the re-indent claim is corrected (TASK-4035)
--format json prints the server's JSON re-indented without the typed
decode; table and markdown keep it. The MCP description names reopened.
Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5
|
||
|
|
acbe837467 |
test(server): the re-register test's row no longer sits on the expiry boundary (BUG-4033) (#2289)
TestPushDispatch_ReregisterKeepsPendingRowDeliverable aged its row exactly
pushExpireAfter (1h). The fixture stamps rows from the real clock, truncated
to the second, and the tick runs on the real clock, so whenever a second
boundary fell between the insert and the tick the row expired instead of
being sent ("row = expired/\"\", want sent").
The row is now 30m old: still after the device's original registration 2h
ago, which is what the test pins, and well inside the expiry window. The
bound itself is unchanged (ruled, c23cebd2).
Red control: a 1.1s sleep between insert and tick failed the old test 3/3
with the exact symptom and passes the fixed test 3/3. A mutant that resets
created_at on re-register still fails the fixed test (skipped/predates_device).
Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5
|
||
|
|
8204f03d25 |
feat(server): push dispatch timings are configurable, and a bad value refuses startup (TASK-3939) (#2288)
* feat(server): push dispatch timings are configurable, and a bad value refuses startup (TASK-3939) Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * docs(deployment): push gateway env vars, dispatch timings and the cleartext warning (TASK-3939) Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * fix(server): the lease refusal names the cause, and an overflowing margin is refused (TASK-3939) SetPushDispatchConfig wraps errPushLeaseTooShort with the resolved lease, send timeout and margin; configurePush names both knobs and appends it. A margin within sendTimeout of MaxInt64 is refused before the sum can wrap. Whitespace-only values stay malformed and are now documented and tested; values are otherwise uncapped. Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 |
||
|
|
2bacd54558 |
feat(web): the Needs you page at /console/needs (TASK-3923) (#2287)
* feat(web): the Needs you page at /console/needs (TASK-3923) Cross-workspace inbox of what waits on the viewer: grouped by workspace, server-side sort (age default), Dismiss and Defer (marker echoed verbatim, local-time presets), Hidden view with Restore, Load more, degraded notice. Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * fix(web): needs page: copy, acted-row filter, failed-reload counts, one defer menu (TASK-3923 r1) - Hidden view copy: dismissals return on news, deferrals at their time. - Rows acted on are filtered from any list request that started before the action completed (first load, Load more, quick sort/view reload). - A failed reload clears total and workspace counts (hidden is kept: it counts everything hidden, independent of sort and view). - DeferMenu closes on an outside press; the page keeps one menu open. Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * fix(web): a filtered list response has its counts adjusted for the rows dropped (TASK-3923 r2) Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * fix(web): a stale list response triggers one fresh page-1 load; adjustCounts removed (TASK-3923 r3) Class fix: a load or loadMore response whose request started before the latest completed dismiss/defer/restore keeps the display filter and then issues exactly one fresh load of the current view, through the ticket guard. The error path also clears degraded. Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * fix(web): needs page: empty state needs no cursor; a settle that did not apply refetches once (TASK-3923 r4) H-F1: the empty state renders only with no rows and no cursor, and a settle that empties the rows with a cursor set reloads page 1. H-F2: an action that completes without settling the current rows refetches the current view. All refetch paths share one marker, so one action completion refetches at most once. Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * fix(web): the automatic needs refetch is quiet: no Loading swap, errors kept, a failure keeps the list (TASK-3923 r5) Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * test(web): restore an assertion a replace touched in the dismiss test (TASK-3923 r5) Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 |
||
|
|
eaa1b0da16 |
feat(push): start the push dispatcher at server startup when a gateway is set (TASK-3939 B2c) (#2286)
* feat(push): start the push dispatcher at server startup when a gateway is set (TASK-3939 B2c) Server startup now calls configurePush, which sets the gateway URL and, when one is configured, refuses a missing secret, sets the gateway client and starts the dispatcher. With no URL nothing starts: no device can register, so nothing is queued. Shutdown already stops the loop with the server's other background workers. The comments that said nothing starts the dispatcher are corrected. Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * docs(push): configurePush's comment says what happens to queued rows with no gateway (TASK-3939 B2c r1 L1) Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 |
||
|
|
b039f111eb |
fix(push): bound re-sends when the sent record fails after a gateway 'sent' (TASK-4032) (#2285)
* fix(push): a sent push whose record fails is parked, not re-sent every lease (TASK-4032) Park ids in-process when RecordNotificationsSent fails after a gateway sent; retry the record at every tick start (gateway or not), skip parked rows after the claim, count them toward the 30/h cap, and drop an entry once recorded or past the 1h expiry age. A size cap backstops the set. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU * fix(push): hold at the unrecorded cap instead of evicting; never claim parked rows (TASK-4032 codex r1) H1: no eviction from the unrecorded set. At the cap a tick still retries the record, then skips claim and send; one fixed-string WARN on entering the held state and one on leaving it. M1: ClaimPendingNotifications takes an exclude list; the dispatcher passes the parked ids so they cannot fill the claim batch. Adds summary-interaction tests. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU * fix(push): serialize dispatch ticks across Stop/Start; pin the re-send assertion first (TASK-4032 codex r2) M1: Stop does not wait for the tick in flight, so a later Start could run a second tick beside it and both could pass the cap check. runPushDispatchTick now holds tickMu for the whole pass; Stop cancels the in-flight tick's context so a waiter is held only for that tick's unwind. L1: FullUnrecordedSetHoldsClaimsAndSends asserts the Send count before the parked count, so an eviction mutant fails on the re-send itself. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU * fix(push): a partial record failure clears the chunks that landed (TASK-4032 codex r3) RecordNotificationsSent commits one chunk at a time, so a failure can follow chunks that already recorded. It now returns a *RecordSentError carrying how many leading ids landed; retryUnrecorded sorts the parked ids and clears that prefix, so a recorded row is no longer counted twice against the rate cap (once as sent, once as parked). The send path honours the prefix too. Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * fix(push): the claim never selects a row past the expiry age (TASK-4032 r5 H-Q1) A parked entry is dropped once its row is past pushExpireAfter, trusting the expiry sweep to have taken the row. The sweep only logs when it fails, so a tick whose record retry and expire both failed could drop the entry and then claim and push the row again. ClaimPendingNotifications now takes expireCutoff and bounds created_at in both the candidate select and the per-row claim UPDATE. The dispatcher's cutoff is truncated to the second, as the store compares, so the claim's bound and the drop rule are exact complements. Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 * docs(push): the parked set's bound is per process (TASK-4032 r6 C-M1) Claude-Session: https://claude.ai/code/session_01D6M9onL7HhMovW8DZbs6f5 |
||
|
|
6cfe322461 |
feat(push): GET /me/notifications/{id} enrichment and the real-enqueue mention leg (TASK-3939 part B2b) (#2284)
* feat(push): GET /me/notifications/{id} enrichment and the real-enqueue mention leg (TASK-3939 part B2b)
The endpoint resolves a push's nid through the dispatcher's own
notificationAccess gate; every denial is one byte-identical 404 and the
recipient's notification settings are never consulted. The mention leg
drives a real comment write through the real enqueue into a dispatch tick.
Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU
* fix(push): enrichment refusals are tested through the real router, docs corrected (TASK-3939 B2b codex r1)
A disabled account is refused 403 account_disabled by RequireAuth for every
id, not 404; docs/api.md said otherwise. Tests now go through ServeHTTP: a
disabled user's real and unknown ids are byte-identical, and an installed
app's padapp_ token (401 on this route) answers identically for both. The
direct-handler tests are gone. docs/api.md also states a 500 is an
operational error, not a denial.
Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU
* test(push): an unpinned API token reads its own notification through the router (TASK-3939 B2b codex r2)
Pins the docs/api.md claim that any authenticated person, an API token
included, can resolve their own notification.
Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU
* fix(push): enrichment returns 500 on an owner or actor lookup error (TASK-3939 B2b codex r3)
GetUser errors were ignored, so a store failure could answer a partial 200
with an empty path owner or actor name. A missing user is still (nil, nil)
and an empty field; only a real error is now a 500, as docs/api.md says.
Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU
|
||
|
|
2a98237e42 |
feat(push): summary push, 30/h cap, retry matrix and expire/prune sweeps (TASK-3939 part B2a) (#2283)
* feat(push): summary push, 30/h cap, retry matrix and expire/prune sweeps (TASK-3939 part B2a) Per-user groups above three collapse into one summary push counting rows; a user is capped at 30 pushed rows per hour. Gateway results are settled per message: only invalid_token deletes a device (and only while the row still holds the sent token), rejected and a refused request skip, retries release with a lease-long backoff. Pending rows expire after an hour and settled rows are pruned after seven days, at most hourly. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU * fix(push): invalid_token deletes only the registration that was sent (TASK-3939 B2a codex r1-M2) DeletePushDeviceIfToken checked (id, token); a same-user same-token re-register updates the row in place, keeping both, so a late invalid_token deleted the fresh registration. LivePushDevices now carries reg_seq and the delete is DeletePushDeviceIfCurrent(id, token, reg_seq). Adds the same-token re-register test, a positive control and an exact-hour boundary test for the cap window; corrects the comments that claimed the token check protected a fresh registration. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU * docs(store): the rate cap's cutoff truncates to the second on purpose (TASK-3939 B2a codex r2) sent_at is stored at whole seconds, so a row in the cutoff's own second cannot be ordered against it; counting the whole second errs toward fewer pushes, never past the cap. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU * fix(push): a tick that stops at the deadline still warns for an unavailable chunk (TASK-3939 B2a codex r3) The deadline check returned before the per-tick WARN, so an earlier chunk's ErrUnavailable went unlogged when a later chunk had no time left. It now breaks out of the chunk loop instead. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU |
||
|
|
9c0637b1a4 |
chore(nix): heal vendorHash for the module set in 837ce48fcb (TASK-2954)
|
||
|
|
837ce48fcb |
chore(ci)(deps): bump the actions-minor-and-patch group with 4 updates (#2234)
Bumps the actions-minor-and-patch group with 4 updates: [actions/setup-node](https://github.com/actions/setup-node), [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/download-artifact](https://github.com/actions/download-artifact) and [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action). Updates `actions/setup-node` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1) Updates `actions/upload-artifact` from 7.0.1 to 7.0.2 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9) Updates `actions/download-artifact` from 8.0.1 to 8.0.2 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c...9000827ccba6bdab643e8b6fd33ac0654aef8333) Updates `goreleaser/goreleaser-action` from 7.2.3 to 7.3.0 - [Release notes](https://github.com/goreleaser/goreleaser-action/releases) - [Commits](https://github.com/goreleaser/goreleaser-action/compare/f06c13b6b1a9625abc9e6e439d9c05a8f2190e94...a4f02ab5355a589de60d917a659ce617ed19b3de) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-minor-and-patch - dependency-name: actions/upload-artifact dependency-version: 7.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-and-patch - dependency-name: actions/download-artifact dependency-version: 8.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-minor-and-patch - dependency-name: goreleaser/goreleaser-action dependency-version: 7.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
cf95e2cb44 |
chore(deps)(deps): bump the npm-minor-and-patch group (#2235)
Bumps the npm-minor-and-patch group in /web with 5 updates: | Package | From | To | | --- | --- | --- | | [dompurify](https://github.com/cure53/DOMPurify) | `3.4.16` | `3.4.17` | | [@playwright/test](https://github.com/microsoft/playwright) | `1.63.0` | `1.64.0` | | [marked](https://github.com/markedjs/marked) | `18.1.0` | `18.1.1` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.3.3` | `8.3.4` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.3` | `5.0.4` | Updates `dompurify` from 3.4.16 to 3.4.17 - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.16...3.4.17) Updates `@playwright/test` from 1.63.0 to 1.64.0 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.63.0...v1.64.0) Updates `marked` from 18.1.0 to 18.1.1 - [Release notes](https://github.com/markedjs/marked/releases) - [Commits](https://github.com/markedjs/marked/compare/v18.1.0...v18.1.1) Updates `vite` from 8.3.3 to 8.3.4 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.3.4/packages/vite) Updates `vitest` from 5.0.3 to 5.0.4 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.4/packages/vitest) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: "@playwright/test" dependency-version: 1.64.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: marked dependency-version: 18.1.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: vite dependency-version: 8.3.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: vitest dependency-version: 5.0.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
9b2998c358 |
chore(deps)(deps): bump the go-minor-and-patch group with 10 updates (#2233)
Bumps the go-minor-and-patch group with 10 updates: | Package | From | To | | --- | --- | --- | | [github.com/alicebob/miniredis/v2](https://github.com/alicebob/miniredis) | `2.39.0` | `2.40.0` | | [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.3.2` | `5.4.0` | | [github.com/pb33f/libopenapi](https://github.com/pb33f/libopenapi) | `0.41.5` | `0.41.6` | | [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) | `1.24.1` | `1.25.0` | | [github.com/prometheus/common](https://github.com/prometheus/common) | `0.72.0` | `0.72.1` | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.57.0` | `0.58.0` | | [golang.org/x/image](https://github.com/golang/image) | `0.46.0` | `0.47.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.46.0` | `0.47.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.42.0` | `0.43.0` | | [golang.org/x/tools](https://github.com/golang/tools) | `0.51.0` | `0.52.0` | Updates `github.com/alicebob/miniredis/v2` from 2.39.0 to 2.40.0 - [Release notes](https://github.com/alicebob/miniredis/releases) - [Changelog](https://github.com/alicebob/miniredis/blob/master/CHANGELOG.md) - [Commits](https://github.com/alicebob/miniredis/compare/v2.39.0...v2.40.0) Updates `github.com/go-chi/chi/v5` from 5.3.2 to 5.4.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.3.2...v5.4.0) Updates `github.com/pb33f/libopenapi` from 0.41.5 to 0.41.6 - [Release notes](https://github.com/pb33f/libopenapi/releases) - [Commits](https://github.com/pb33f/libopenapi/compare/v0.41.5...v0.41.6) Updates `github.com/prometheus/client_golang` from 1.24.1 to 1.25.0 - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/v1.25.0/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.24.1...v1.25.0) Updates `github.com/prometheus/common` from 0.72.0 to 0.72.1 - [Release notes](https://github.com/prometheus/common/releases) - [Changelog](https://github.com/prometheus/common/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/common/compare/v0.72.0...v0.72.1) Updates `golang.org/x/crypto` from 0.57.0 to 0.58.0 - [Commits](https://github.com/golang/crypto/compare/v0.57.0...v0.58.0) Updates `golang.org/x/image` from 0.46.0 to 0.47.0 - [Commits](https://github.com/golang/image/compare/v0.46.0...v0.47.0) Updates `golang.org/x/term` from 0.46.0 to 0.47.0 - [Commits](https://github.com/golang/term/compare/v0.46.0...v0.47.0) Updates `golang.org/x/text` from 0.42.0 to 0.43.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.42.0...v0.43.0) Updates `golang.org/x/tools` from 0.51.0 to 0.52.0 - [Release notes](https://github.com/golang/tools/releases) - [Commits](https://github.com/golang/tools/compare/v0.51.0...v0.52.0) --- updated-dependencies: - dependency-name: github.com/alicebob/miniredis/v2 dependency-version: 2.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: github.com/go-chi/chi/v5 dependency-version: 5.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: github.com/pb33f/libopenapi dependency-version: 0.41.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-minor-and-patch - dependency-name: github.com/prometheus/client_golang dependency-version: 1.25.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: github.com/prometheus/common dependency-version: 0.72.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-minor-and-patch - dependency-name: golang.org/x/crypto dependency-version: 0.58.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: golang.org/x/image dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: golang.org/x/term dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: golang.org/x/text dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: golang.org/x/tools dependency-version: 0.52.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> |
||
|
|
d97c528d1c |
fix(store): push token ownership follows a registration sequence, not updated_at (TASK-4031) (#2282)
Two users sharing a push token were ordered by updated_at (second resolution) then id (a random uuid), so a same-second reclaim could go to the previous registrant. Add push_devices.reg_seq (SQLite 157, Postgres 125): unique, advanced by every register (INSERT and the same-user in-place UPDATE), untouched by the password-rotation session rebind. The backfill orders by (updated_at, id) so no current owner changes. LivePushDevices compares reg_seq. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU |
||
|
|
ba3a0ab69b |
fix(store): a same-user push re-register keeps the device's created_at and row (TASK-4027) (#2281)
Ownership of a token shared across users now follows updated_at, so a reclaiming user is not outranked by the previous registrant. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU |
||
|
|
dbbbefda75 |
feat(push): the push dispatcher core and its privacy gates, not yet wired (TASK-3939, part B1) (#2280)
* feat(store): record a delivery by id, unclaim without an attempt, re-check holds (TASK-3939 B1) RecordNotificationsSent is token-free for pending|expired rows (r4-L4), UnclaimNotifications hands never-tried rows back without an attempt, HeldNotifications is the pre-send re-check. The claim error drops the row id (Q8), notify.SendTimeout is exported (Q4), and the store doc says SENT is the one deliberate exception to settle-under-the-claim (Q5). Claude-Session: https://claude.ai/code/session_018mVC2RwgKtXviHMayjw2pg * feat(server): the push dispatcher core and its privacy gates (TASK-3939 B1) Claims settled rows, runs membership, liveness, visibility, assignment, comment, settings, agent-activity and device gates on each (a skip carries a recorded reason and never reaches the gateway), groups survivors per (user, item), sends one content-free push per live device and settles per chunk. Lease >= sendTimeout + margin is enforced by the setter, the send deadline and a per-chunk time check. Not wired into cmd/pad. Claude-Session: https://claude.ai/code/session_018mVC2RwgKtXviHMayjw2pg * test(server): dispatcher gates, lease invariant and late-send recording (TASK-3939 B1) Claude-Session: https://claude.ai/code/session_018mVC2RwgKtXviHMayjw2pg * test(server): an unknown notification kind is skipped (TASK-3939 B1) Claude-Session: https://claude.ai/code/session_018mVC2RwgKtXviHMayjw2pg * fix(server): the dispatcher logs no ids, settles zero-message units, checks the comment's item and has a clock seam (TASK-3939 B1) C1 reword the store header (the dispatcher is wired in part B2). H1 split computeWatchAccessVisibility into a log-free core the dispatcher calls; the handler's wrapper logs exactly as before. H2 settle a unit with no message as a no_device skip. H4 a mention/comment/reply row's comment must be on the row's item and workspace. Add the unexported now seam. T3 comment on the nil user branch. Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU * test(store): settle primitives assert attempts, sent_at and span more than one id chunk (TASK-3939 B1) Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU * test(server): dispatcher fix round, clock-seam lease tests, every gate error, gate order, log leak, zero-message unit (TASK-3939 B1) Claude-Session: https://claude.ai/code/session_01SSofJ73Kn2s3W4TTnjoAfU |
||
|
|
aad3c35bdb |
fix(cli): pad item list --format json pages to completion when --limit is not set (BUG-4024) (#2279)
* fix(cli): pad item list --format json pages to completion when --limit is not set (BUG-4024) `pad item list --format json` stopped at the CLI's own 200 default and said so on stderr only, so a consumer parsing the array saw 200 of 204 rows with no signal in the payload. Ruling: with --format json and no --limit (cobra Changed, not a value compare) the CLI follows X-Pad-Next-Offset to the end, sends no limit and prints no notice. The payload stays a plain array; no API, schema or MCP change. An explicit --limit, including --limit 0, keeps today's behaviour and its stderr notice. --limit 0 has always meant "the default" (clamped to 200), not "no limit"; a test pins that. Table and markdown are untouched. --offset with no --limit pages from that offset to the end. One private pager (listAllPages) now backs ListAllItems and ListAllCollectionItems (the old names stay as wrappers) and new *Report variants taking a start offset and returning the first page's undeclared filter keys, so the BUG-3480 warning still fires. A non-advancing next offset, or more than 1000 pages, is an error and a non-zero exit with a message naming the cap, never a partial array. This also applies to the existing callers (reconcile, github status, project standup), which used to return a short list for the non-advancing case. Claude-Session: https://claude.ai/code/session_013aLLQx2EARjEzmWHbExjtm * fix(cli): the list pager requires has-more pages to be full and contiguous (BUG-4024 R2-F1) listAllPages accepted any next offset greater than the current one, so an empty has-more page, or a next offset that skipped rows never returned, followed by a has-more=false page, ended the loop on a partial list with no error. The server sets has-more only when it trimmed the probe row (handlers_items.go listItemsPage), so such a page holds exactly `limit` rows and next-offset == offset+len(items) with len(items) > 0. The pager now requires exactly that and otherwise returns an error with nil items. The check subsumes the old `NextOffset <= offset` test. Tests: empty page (advancing and non-advancing next offset), a jump, and one short, at the client level; the jump at the `pad item list --format json` level (non-zero exit, empty stdout). Claude-Session: https://claude.ai/code/session_013aLLQx2EARjEzmWHbExjtm * fix(cli): the list pager reads a negative start offset as 0, as the server does (BUG-4024 R3-F1) Claude-Session: https://claude.ai/code/session_01UiMM6MkrYr1nkuaNkSmkeD |
||
|
|
20a150064f |
feat(push): gateway client and notification claim lifecycle (TASK-3939, part A of 3) (#2278)
* feat(push): gateway client and notification claim lifecycle (TASK-3939, part A) internal/notify: the push gateway client (X-Cloud-Secret, no redirects, capped response, 400 -> ErrBadRequest, everything else -> ErrUnavailable, results pinned to the request) and the fixed content-free bodies. internal/store: claim/settle/expire/release/prune for user_notifications, the live-device query (session joined on id and user_id and unexpired; the newest live registrant of a shared token wins), and the item liveness read. PAD_PUSH_GATEWAY_SECRET with the cloud outbound secret as fallback; startup fails when a gateway URL is set and no secret resolves. No dispatcher yet. Claude-Session: https://claude.ai/code/session_01VQbeqVujjtTDyGqeYj6wxE * test(push): shared-token tie in both directions and no-leak log capture (TASK-3939, part A) Claude-Session: https://claude.ai/code/session_01VQbeqVujjtTDyGqeYj6wxE * fix(push): claim read-back is scoped to the call; drain gateway bodies; config no longer imports billing (TASK-3939, part A r1) Claude-Session: https://claude.ai/code/session_01VQbeqVujjtTDyGqeYj6wxE * fix(push): each claim gets its own token; the drain test proves connection reuse (TASK-3939, part A r2) ClaimPendingNotifications stamps caller token + per-call random nonce and returns it as Claim.Token; mark, skip and release take that token, so reusing a caller token in the same second cannot read back another call's rows. The gateway drain test now asserts connection reuse via httptrace. Claude-Session: https://claude.ai/code/session_01VQbeqVujjtTDyGqeYj6wxE * test(notify): derive the drained body size from maxResponseBytes (TASK-3939 r3-L1) Claude-Session: https://claude.ai/code/session_01TUNioUjEdnMe6FWZcaGxzm * fix(store): clamp a notification claim to 1..notify.MaxBatch (TASK-3939 r4-L3) SQLite reads a negative LIMIT as unbounded and the read-back IN list is not chunked. Also labels the no-secret-in-errors gateway test as a pin (r4-L5). Claude-Session: https://claude.ai/code/session_01TUNioUjEdnMe6FWZcaGxzm * test(store): a zero claim limit clamps to 1 (TASK-3939 r4-L3) Claude-Session: https://claude.ai/code/session_01TUNioUjEdnMe6FWZcaGxzm * fix(store): a claim limit <= 0 claims nothing and writes nothing (TASK-3939 r5-M1) Replaces the 0 -> 1 promotion: a caller asking for no rows must not be handed a lease nobody will send. A limit above notify.MaxBatch still clamps to it, so "never exceeds limit" holds as written. Claude-Session: https://claude.ai/code/session_01TUNioUjEdnMe6FWZcaGxzm |
||
|
|
d6dcc2d89b |
fix(store): decision last_error is sanitised and cut at a rune boundary (BUG-4018) (#2277)
* fix(store): decision last_error is sanitised and cut at a rune boundary (BUG-4018)
A provider response body is arbitrary bytes. The byte cut at 1024 could split
a rune, and invalid UTF-8 or a NUL made the last_error UPDATE fail on
Postgres (SQLSTATE 22021 / the NUL guard), leaving the failure unrecorded.
truncateDecisionError now repairs NULs, replaces invalid UTF-8, then cuts at
a rune start. Corrects the decision_jobs.last_error NUL exemption reason.
Claude-Session: https://claude.ai/code/session_01HUKE6m5Ps66HAiDpJ3QMQ8
* test(store): exact boundary cuts and sanitise-before-cut ordering for decision errors (BUG-4018 r1)
Boundary test asserts the exact cut for 2-, 3- and 4-byte runes; new
FailDecisionJob subtests pin sanitise-then-cut. nulcolumns reason says
last_error may embed the provider body.
Claude-Session: https://claude.ai/code/session_01HUKE6m5Ps66HAiDpJ3QMQ8
* test(store): ASCII just over the decision-error limit is cut to exactly 1024 bytes (BUG-4018 r2)
Every boundary case carried a rune of width >= 2, so the shortest
over-limit input was 1026 bytes and a limit check off by one byte
(len(s) <= max+1) passed. 1025 and 2000 plain ASCII bytes now pin the cut.
Claude-Session: https://claude.ai/code/session_01HUKE6m5Ps66HAiDpJ3QMQ8
* test(store): the off-by-one limit comment states what was measured (BUG-4018 r2)
The previous commit's comment said no earlier boundary case could catch a
limit check off by one byte. Measured at
|
||
|
|
dd93b7cbc2 |
feat(web): Notifications section in account settings (TASK-3940) (#2276)
* feat(web): Notifications section in account settings (TASK-3940) Claude-Session: https://claude.ai/code/session_01VQbeqVujjtTDyGqeYj6wxE * fix(web): notification toggles keep focus while saving; #notifications deep link scrolls (TASK-3940 r1) Claude-Session: https://claude.ai/code/session_01VQbeqVujjtTDyGqeYj6wxE |
||
|
|
527fa47e0f |
fix(store): a user read no longer decrypts the TOTP secret; only the second-factor check does (BUG-4019) (#2275)
* fix(store): a user read no longer decrypts the TOTP secret; only the second-factor check does (BUG-4019) models.User no longer carries the secret. GetUserQ and its siblings, the member and backlink reads and the mention-recipient check therefore cannot fail on a secret that will not decrypt. checkSecondFactor loads it through the new TOTPSecretForVerify after the limiter charge: no secret refuses the TOTP branch (an empty one never verifies), a decrypt failure logs once without secret material and fails closed, and the recovery-code branch is untouched. Claude-Session: https://claude.ai/code/session_01DQDip8AC96dA71t2UCPLTD * fix(server): the undecryptable-secret path logs once; the read tests reach the row (BUG-4019 r1) - checkSecondFactor writes its 500 directly: writeInternalError logged the same error a second time. T2 counts every ERROR record except the request logger's per-request line. - T1's Stripe lookup now matches the undecryptable user (it looked up an unknown customer and could not go red), and ListUsers and SearchUsers are pinned to return the row with its 2FA flag. - The appstore boundary allowlist no longer says GetUserQ decrypts, and drops the deleted decryptUserTOTP. - Two comments no longer claim a single reader of the column. Claude-Session: https://claude.ai/code/session_01HUKE6m5Ps66HAiDpJ3QMQ8 * fix(server): 2FA on with no secret behind it warns the operator (BUG-4019 r2) An enabled row whose secret is empty cannot verify a code, so the user is held to recovery codes; checkSecondFactor now logs one warning naming the user. T5 asserts it. The door tests assert exactly 500, not any 5xx, and the test placeholder helper drops its unused argument. Claude-Session: https://claude.ai/code/session_01HUKE6m5Ps66HAiDpJ3QMQ8 * fix(server): the empty-secret warning claims only what is true at that instant (BUG-4019 r3) user.TOTPEnabled is the request's snapshot, and a disable landing after it also reads as no secret, so the warning no longer says 2FA is enabled; it says there was no secret to verify a code against. T5 asserts the new text. Claude-Session: https://claude.ai/code/session_01HUKE6m5Ps66HAiDpJ3QMQ8 * test(store,server): a stored secret that decrypts to empty is no secret (BUG-4019) The decrypted=="" guard in TOTPSecretForVerify was unexercised: the empty case wrote '' and returned at stored=="" first, and encrypt("") returns "" rather than sealing it, so dropping the guard left every test green. The store test now writes a hand-sealed empty plaintext to an enabled row and expects ErrNoTOTPSecret; a server test refuses a code computed from "" on that row. Claude-Session: https://claude.ai/code/session_01HUKE6m5Ps66HAiDpJ3QMQ8 * test(server): the BUG-4019 test header names the 2FA paths, not one check (BUG-4019 r5) EnableTOTP also decrypts the stored secret (to compare) and fails on one that will not decrypt, so the header no longer says the second-factor check is the only thing that fails. TOTPSecretForVerify remains the only reader that returns the plaintext. Comment only. Claude-Session: https://claude.ai/code/session_01HUKE6m5Ps66HAiDpJ3QMQ8 |
||
|
|
ca184cff96 |
feat(needs): sort=importance|simplicity with a score-keyed cursor (TASK-3959, part 2) (#2274)
* feat(needs): sort=importance|simplicity with a score-keyed cursor (TASK-3959, part 2) GET /me/needs takes sort=importance and sort=simplicity: the primary score then the secondary, both highest first, then the item id; a row a scorer left unscored sorts last. The cursor carries the key as score/score2 (absent under age, so an age cursor is unchanged), and a score cursor missing either, or out of -1..100, is invalid_cursor. docs/api.md documents the sorts. Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw * fix(needs): r1 — differing-order sort test, degraded-page and missing-score docs, comments (TASK-3959, part 2 r1) Claude-Session: https://claude.ai/code/session_01DQDip8AC96dA71t2UCPLTD * fix(needs): r1 — range-check every cursor field, pin the as_of clamp (TASK-3959, part 2 r1) Claude-Session: https://claude.ai/code/session_01DQDip8AC96dA71t2UCPLTD * fix(needs): a cursor the server minted is always followed; key times clamp to int64 nanoseconds (TASK-3959, part 2 r2) r2 found that r1's key/created_at range check (> 0, before 2200) refused cursors the server itself mints: a pre-1970 date, or a zero time from an unparseable stored created_at, gives a non-positive UnixNano, and paging dead-ended at that row under every sort. A check may refuse only what the server could never mint, so key and created_at are no longer checked. A time outside int64 nanoseconds (a zero time, or past 2262) also did not round-trip: UnixNano wraps, so an age cursor named a different instant from the row's key. KeyFor now clamps both key times to that range, so the sort and the cursor see the same instant. The table drops the key/created_at 400 rows, keeps negative values as followed, and adds the age-cursor-with-a-score rows (r2 L2). A new test pages pre-1970, zero, unparseable and year-3000 boundary rows through every sort at limits 1 and 2. Claude-Session: https://claude.ai/code/session_01DQDip8AC96dA71t2UCPLTD * fix(needs): the cursor docs say only what the server could never mint is refused; the odd-time test runs on Postgres (TASK-3959, part 2 r3) r3 (both reviewers): docs/api.md still listed the key/created_at range check removed in r2. The sentence now lists exactly what is refused, says the key times are positions that any value of follows, and says a key time outside about 1677 to 2262 is clamped, so such rows tie on it (hunter L3). The odd-boundary test wrote created_at through the raw *sql.DB with a ? placeholder, which is not rewritten for Postgres; it now writes literal SQL, as setCreated does, and passes on both backends. It reports each sort with Errorf, and its comment says what each sort proves: every sort that the cursor's key times are followed, and only age that the clamp holds (r3 M2). Claude-Session: https://claude.ai/code/session_01DQDip8AC96dA71t2UCPLTD * fix(needs): the cursor docs list the version check; the real-scorer sort test runs on both backends (TASK-3959, part 2 r4) r4 hunter L1: the sentence listing exactly what a cursor check refuses left out a cursor of another version (v). r4 hunter L2: the test that sorts by the real scorers through the router ran on SQLite only; it now runs under needsBackends, and passes on Postgres. Claude-Session: https://claude.ai/code/session_01DQDip8AC96dA71t2UCPLTD |
||
|
|
5aa9e49ab4 |
feat(needs): importance and simplicity scorers on /me/needs (TASK-3959, part 1b) (#2273)
* feat(needs): importance and simplicity scorers on /me/needs (TASK-3959, part 1b)
Registers the two needs scorers in server.New. Each fills needs.Score
{Value, Hint, Why} on every row, reading through the part 1a batch reads
and a request-scoped memo, so one request does each read once.
- Importance: round(100 x (.25P + .20B + .20D + .15S + .10H + .10A)).
B is a breadth-first fan-out over blocks links (visited set, depth 5,
cap 99). It counts and expands only targets visible under the scope and
still open, and returns a count, never refs. S is 0 until the
high_stakes eval (TASK-4011, ruling 440f5c63), so importance is capped
at 85 and not renormalised.
- Simplicity: (3 - effort)/3 x 100. The effort level is the argmax and the
hint comes from a fixed table (cap 40). With no decision runner, effort
is the text heuristic, with no hint.
- needs.Viewer gains Now, set from asOf, as the scorers' clock.
- Why emits only a fixed vocabulary, listed in docs/api.md; a test holds it
to that list.
The PLAN-3535 guard classes the new IsTerminalItem call as "keep".
Rulings 99c2bcfe and 6901f2f5 on the TASK-3959 trail.
Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw
* fix(needs): pass the workspace to the scoped decision and job reads (TASK-3959, part 1b)
Follows part 1a's round-1 fix, which scopes LatestDecisionsForItems and OwedDecisionJobs to a workspace. The needs memo passes its own workspace id.
Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw
* fix(needs): fail closed on scope and unknown collections, local-day deadline, request ctx (TASK-3959, part 1b r1)
Leak test through the router for a restricted viewer, fail-closed fan-out
pins, isOpen fails closed for an unknown collection, the question-hash read
takes the request ctx, D measures the server-local day like the dashboard,
H is clamped to [0,1], and docs/api.md's degraded prose names the real causes.
Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw
* fix(needs): pin every importance input, overdue parity with the dashboard, snapped rounding (TASK-3959, part 1b r2)
Per-input contribution table; overdue decided by itemOverdue; weighted sum
snapped before rounding; injectable location instead of mutating time.Local;
restricted-viewer score compared exactly to the hidden-item-deleted score;
docs: overdue rule and the WorkspaceFlagsForItems degraded path.
Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw
* test(needs): pin the deadline key pick, priority case and unreadable effort; scores at as_of (TASK-3959, part 1b r3)
- The importance parts table pins the earlier of due_date and end_date
(either key earlier, end_date alone) and the case-insensitive priority.
- A current effort answer whose level cannot be read falls back to the
text heuristic with no hint; a test now pins it, and docs/api.md says so.
- docs/api.md: scores are computed at the request's as_of, which the
cursor carries to later pages.
Claude-Session: https://claude.ai/code/session_014QJdFAhYDf79NBf2udvfdY
* test(needs): as_of reaches the scorers through the handler; threshold, key and date pins (TASK-3959, part 1b r4)
- A request with a cursor scores at the cursor's as_of, end to end through
ServeHTTP (page 1 at the wall clock is 33; a cursor a week after
waiting_since is 28).
- "judged to need a human" from the attention threshold up, not below.
- Unreadable effort keys -1 and x fall back to the heuristic, like 7.
- A date too short to parse scores D=0.
- blocks() states the lock it runs under.
Claude-Session: https://claude.ai/code/session_014QJdFAhYDf79NBf2udvfdY
* test(needs): the as_of check is its own test, so the binding test stays one claim (TASK-3959, part 1b r4)
Claude-Session: https://claude.ai/code/session_014QJdFAhYDf79NBf2udvfdY
* test(needs): a failed read degrades both real scorers; short-date overdue; effort without a medium (TASK-3959, part 1b r5)
Claude-Session: https://claude.ai/code/session_014QJdFAhYDf79NBf2udvfdY
* docs(needs): every scorer sharing a memo passes the same rows (TASK-3959, part 1b r6)
Claude-Session: https://claude.ai/code/session_014QJdFAhYDf79NBf2udvfdY
* fix(needs): needs-human weighs only an answer the inbox's flag rule holds current (TASK-3959, part 1b r7)
H and "judged to need a human" read needs_human through the flag path
(WorkspaceFlagsForItems at threshold 0 over the rows), so an answer made
stale by an edit, with no refresh owed, no longer weighs or says so while
the router does not flag the item. Effort keeps question-hash currency;
docs/api.md states both rules. EffortMediumMinProb is pinned at exactly .5.
Claude-Session: https://claude.ai/code/session_014QJdFAhYDf79NBf2udvfdY
* test(needs): a needs-human answer held while its refresh is owed; the asked-row exception in the docs (TASK-3959, part 1b r8)
Claude-Session: https://claude.ai/code/session_014QJdFAhYDf79NBf2udvfdY
|
||
|
|
423d312b77 |
feat(store): @mentions enqueue a push notification (TASK-3946) (#2272)
* feat(store): @mentions enqueue a push notification (TASK-3946) A comment create or edit enqueues a 'mention' notification for each user the comment_mentions index newly records, keyed mention:<comment>:<user>. Never on a tombstone, import, cross-workspace copy or the backfill: the reindex returns the new users and only the create and edit sites enqueue. The author is never told. A mentioned user who cannot see the item (non-member, no-grant guest, restricted member outside the collection) gets nothing (binding rule 645fab20). On create a mention supersedes the user's comment/reply row; on edit a written mention deletes the user's still-pending comment/reply row for the comment, and a row already sent, skipped or expired stays (ruling f0fc5933). Claude-Session: https://claude.ai/code/session_019KLyQQPbv9eCe1d6PT2wHP * fix(server): an edit's mentions are from the editor (TASK-3946 r1) The edit handler passes the request's actor to a new UpdateCommentAs, so a mention a platform admin adds to another user's comment is from the admin, the admin's own @-mention is skipped, and the author is told when mentioned (ruling e6a86278). UpdateComment(id, body) stays for tests and attributes to the author. Claude-Session: https://claude.ai/code/session_019KLyQQPbv9eCe1d6PT2wHP * test(appstore,store): review the mention helpers into the fence door (TASK-3946) The fenced comment create and edit now reach the mention enqueue and its visibility read. Each newly reachable helper joins allowedStoreMethodsInsideDoor with what it writes, confirmed by reading it: only enqueueNotificationsCountTx (the existing INSERT) and enqueueMentionNotificationsTx (the pending comment/reply supersede DELETE) write, on the fence's tx. The mention site guard follows the UpdateComment -> updateComment rename. Claude-Session: https://claude.ai/code/session_019KLyQQPbv9eCe1d6PT2wHP * docs(store): qualify the reply-wins note; GetUserQ decrypts (TASK-3946 r4) The one-"reply" rule on enqueueCommentNotificationsTx yields to a mention, and the fence door's GetUserQ entry names the in-memory TOTP decrypt (ruling 864f5506). Claude-Session: https://claude.ai/code/session_019KLyQQPbv9eCe1d6PT2wHP |
||
|
|
94191b2bdc |
feat(needs): store reads for importance and simplicity scoring (TASK-3959, part 1a) (#2271)
* feat(needs): store reads for importance and simplicity scoring (TASK-3959, part 1a) GetBlocksEdges now returns each blocked item's collection and fields as well (TargetCollectionID, TargetFields). The needs scorers walk blocks forward and count only targets the viewer may see that are still open. New batch reads for the needs scorers (internal/store/needs_scoring.go) and the effort answer reads (internal/decision/effort_read.go). The stored answer shape is private to the decision package, so the reads live there. The effort_medium rule (a top probability under .5 reads as unclear) is EffortMediumMinProb. Nothing calls these yet. The scorers that do are part 1b. Split at the store/server seam by lead ruling 6901f2f5 on the TASK-3959 trail. Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw * fix(needs): scope the decision and job reads to the workspace; a NULL body reads as empty (TASK-3959, part 1a r1) Review round 1 on part 1a (codex and hunter): - LatestDecisionsForItems and OwedDecisionJobs now take the workspace and read only live items of it, scoped through the item's own workspace_id, like NeedsScoringItems. Before, they trusted the caller's ids: OwedDecisionJobs answered for a deleted item, and neither checked the workspace. - NeedsScoringItems reads COALESCE(length(content), 0). items.content is nullable, and one NULL failed the Scan for its whole chunk. - The CurrentQuestionHashes comment says what a hash match does and does not prove: the question is unchanged, not the item (ruling 99c2bcfe, 3). Tests cover the other-workspace and deleted-item cases for both reads, a NULL body, and LatestDecisionsForItems over the IN-list chunk limit. Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw * test(needs): the effort readers: argmax, ties, unreadable answers (TASK-3959, part 1a r2) Review round 2 on part 1a: EffortLevel, ChoiceValue and CurrentQuestionHashes had no tests of their own. The new tests cover: - argmax rather than a truncated mean (a distribution whose mean is 1.7 and whose argmax is 2); - a tie going to the lower level; - a stray or out-of-range winning key reading as unreadable, which fails safe; - other kinds and malformed answers; - ChoiceValue's weak, no-distribution and no-choice cases; - a nil runner. Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw * test(needs): discriminating tests for hashes, duplicate ids and blocks targets (TASK-3959, part 1a r3) Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw * test(needs): compare blocks-edge fields as JSON, not text (TASK-3959, part 1a) make test-pg failed TestGetBlocksEdges_CarriesTheTarget: Postgres stores items.fields as JSONB and returns it with keys reordered and spaced, so an exact string comparison cannot hold there. The fields are now compared as parsed JSON. The source and target still carry different values, so a source/target swap in the query still fails the test. Claude-Session: https://claude.ai/code/session_01UghmjfBZb6A8UbRgi4rPaw |
||
|
|
d76f06d199 |
feat(server): push device registration and notification settings API (TASK-3938) (#2269)
* feat(store): push device register/delete, SessionInfo.ID, one-tx session rotation (TASK-3938) RegisterPushDevice is one transaction: delete rows on either unique key, evict the oldest at the 20-device cap, insert. RotateUserSessions mints the replacement, rebinds the caller's own devices, deletes the other sessions, and fails closed to revoke-all. Claude-Session: https://claude.ai/code/session_019KLyQQPbv9eCe1d6PT2wHP * feat(server): push device and notification-settings routes, push_available (TASK-3938) Session-only PUT/DELETE /me/push-devices/{connection_id}, GET/PUT /me/notification-settings, push_available on /auth/session, PAD_PUSH_GATEWAY_URL, and all credential-change rotations through RotateUserSessions. docs/api.md updated. Claude-Session: https://claude.ai/code/session_019KLyQQPbv9eCe1d6PT2wHP * test(server): classify GET /me/notification-settings in the compress census (TASK-3938) Claude-Session: https://claude.ai/code/session_019KLyQQPbv9eCe1d6PT2wHP * fix(server): the pinned-token refusal on notification settings takes BUG-3982's shape (TASK-3938 r1) * fix(server): notification settings leave the pinned-token refusal to the BUG-3982 gate (TASK-3938 r1) * test(store): concurrent registers at 19 devices hold the push-device cap (TASK-3938) Claude-Session: https://claude.ai/code/session_019KLyQQPbv9eCe1d6PT2wHP * test(store): make the concurrent-register cap test able to fail on Postgres (TASK-3938) Reuse sessions across rounds and widen the count-to-insert window with a test-owned slow insert trigger, so removing the user-row lock overshoots the cap on every run. Claude-Session: https://claude.ai/code/session_019KLyQQPbv9eCe1d6PT2wHP * docs(server): a workspace-pinned token on /me/push-devices gets the BUG-3982 refusal, not session_required (TASK-3938 r1) * fix(server): push_available goes to a signed-in caller only (TASK-3938 r2) |