Skip to content

Commit 7e4ca0e

Browse files
authored
Merge pull request #2087 from jaylfc/sync/dev-to-master-beta43
Merge dev into master: v1.0.0-beta.43
2 parents b736cdc + 61a0769 commit 7e4ca0e

19 files changed

Lines changed: 2567 additions & 12 deletions

.claude/skills/taos-development-skill/SKILL.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ A finding folded within hours merges the same day; a finding left while you open
240240
new PRs stalls the whole train behind it (the maintainer will not merge past an
241241
open finding, ever).
242242

243+
Folding means code AND a reply: answer every numbered item in the PR thread
244+
with the commit that addresses it or a concrete rebuttal. Code pushed without
245+
in-thread replies leaves the fold formally open and the verdict at HOLD.
246+
247+
Bot review freshness is part of folding: check WHICH commit a bot actually
248+
reviewed. A rate-limited or stale "SUCCESS" on an older head is not a pass -
249+
after pushing fixes, re-trigger the review (for CodeRabbit: comment
250+
`@coderabbitai review`).
251+
243252
### Rebase cadence and the stale-base rule
244253

245254
- dev moves fast. When your PR shows CONFLICTING, rebase onto current dev
@@ -265,6 +274,64 @@ A close without a successor link reads as lost work and forces the maintainer
265274
into git forensics (this happened with #1927/#1924 - both were legitimate
266275
"landed via" closures that looked like data loss for hours).
267276

277+
### Asking another team's agent a question
278+
279+
taOS depends on sibling services with their own maintainer agents (taOSmd, the
280+
website). Their contracts are theirs to state, so ASK rather than inferring from
281+
their source (pitfall 23). How to reach them depends on where you sit:
282+
283+
- **On the A2A bus** (internal agents): post on the relevant channel, name the
284+
agent, and expect a reply inside the hour.
285+
- **Outside the bus** (external contributors): open an issue on
286+
`jaylfc/taos-agent-commons`, the private invite-only coordination repo. Label
287+
it `contract-question` and name the service. @taOS-dev sweeps it hourly and
288+
relays to the owning agent on the bus, then carries the answer back.
289+
- `jaylfc/taosmd` is public with issues enabled, so a taosmd contract question
290+
can also go straight there.
291+
- `jaylfc/taos-website` is private, so commons or the relay is the only route.
292+
293+
If a question sits unanswered for more than about two hours, escalate by also
294+
raising it on the PR. The relay is a person-shaped hop and can stall; silence
295+
should never be mistaken for progress.
296+
297+
This arrangement is TEMPORARY scaffolding. It retires when an external
298+
contributor can hold a taOS identity and reach the bus directly, which is the
299+
same capability as agent sharing. Do not build tooling that assumes it is
300+
permanent.
301+
302+
### Verify before you claim, and compile before you PR
303+
304+
- An assertion with a tolerance (`abs(a - b) <= n`) does not test an invariant.
305+
It cannot tell correct behaviour from total failure. Assert equality and
306+
assert the resulting state (pitfall 20).
307+
- Mocking internals or injecting unreproducible errors is fine. Mocking an
308+
external service CONTRACT is where tests lie. For sibling services (taOSmd,
309+
the website) the contract has a reachable owner on the A2A bus: ASK them
310+
rather than inferring from their source. Every mock failure in the #2062
311+
cycle was a guess at something one message would have answered. External
312+
contributors ask in the PR and the lead relays. For genuine third parties,
313+
capture a real response and commit it rather than composing a fixture from
314+
what your code expects. Then keep one feature detecting integration test per
315+
contract, or mark the mock provisional in code with its source and date. A
316+
follow-up issue does not count: it separates the caveat from the code. A
317+
green test over a fictional contract certifies the bug (pitfall 23).
318+
- Typecheck or run the thing before opening the PR. A frontend change that does
319+
not compile wastes a full review round, and the executor now gates on
320+
`tsc --noEmit` for exactly that reason (pitfall 21).
321+
- When the base branch has moved under you, rebase and read what changed before
322+
resolving conflicts. Taking the wrong side of a hunk silently reverts fixes
323+
that were just merged (pitfall 22).
324+
325+
### Scope honesty per slice
326+
327+
- The PR body states exactly what it ships versus what its issue scopes. Any
328+
deferred part gets an explicit deferral note AND a follow-up issue filed in
329+
the same push; the parent issue never auto-closes on a partial slice.
330+
- Before committing, `git status` must show only intended source changes -
331+
runtime artifacts (anything under `data/`) never enter a commit, and a new
332+
component that writes under `data/` gitignores its directory in the same PR
333+
(pitfall 16).
334+
268335
### One PR per slice
269336

270337
- A fix and the test that proves it belong in ONE PR (pitfall 13 in

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ data/videos/
3030
data/secrets.db
3131
data/models/
3232
data/workspace/
33+
data/library/
34+
data/collections/
3335
data/*.log
3436

3537
# Environment

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,42 @@ Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotio
77

88
## [Unreleased]
99

10+
## [1.0.0-beta.43] - 2026-07-21
11+
12+
### Added
13+
14+
- **Library app P1**: LibraryStore, ingest pipeline, cheap-tier processors (file,
15+
text, PDF, image) and the collections handoff to taOSmd. Ingested items are
16+
copied into a per-item directory and registered as a taOSmd collection over the
17+
live Collections API, with async index polling and typed link rows (#2062).
18+
- Invite mint accepts an optional `ttl_secs` (60s to 24h) so a longer-lived
19+
invite is a deliberate choice rather than a code change (#2072).
20+
21+
### Fixed
22+
23+
- **Invite dialog crashed the desktop.** The invite list endpoints returned
24+
`scopes` as a JSON-encoded string while the UI typed it as an array, so the
25+
dialog threw and tripped the SPA error boundary whenever any pending invite
26+
existed. This also caused the post-mint refresh to unmount the dialog before
27+
the URL and PIN were shown (#2066).
28+
- **Expired invites could not be revoked.** `revoke` only matched `pending`, so an
29+
invite that lazily flipped to `expired` returned 404 while still listed, leaving
30+
dead rows against the pending cap. Revoke now covers expired, and terminal
31+
states return 409 with the actual state (#2071).
32+
- Default invite TTL raised from 15 minutes to 1 hour. Handing a URL and PIN to a
33+
human who then configures an agent is not a 15 minute flow (#2072).
34+
35+
### Changed
36+
37+
- `docs/getting-started.md` documents the Hailo-10H AI HAT+2 and the Raspberry Pi
38+
5 M.2 slot conflict: the HAT occupies the only M.2 slot, so it cannot be used
39+
alongside an NVMe boot drive (#2075).
40+
- Contributor docs gained seven new defect classes drawn from real review
41+
findings, covering runtime state in commits, mobile view registries, retrofit
42+
migrations on shipped stores, scope honesty, tolerance assertions, shell
43+
snippets in template literals, conflict resolution, and how an external
44+
contributor reaches another team's agent (#2069, #2079).
45+
1046
## [1.0.0-beta.42] - 2026-07-20
1147

1248
### Added

desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tinyagentos-desktop",
33
"private": true,
4-
"version": "1.0.0-beta.42",
4+
"version": "1.0.0-beta.43",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

docs/contributor-pitfalls.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ session + CSRF, or the agent-token allowlist in `auth_middleware.py`) and a
1212
test asserting an unauthenticated or cross-principal caller gets 401/403.
1313
Example: PR #2036 added `GET /api/secrets/agent/{name}/github` with no guard,
1414
leaking installation IDs and repo names to any caller.
15+
For project-scoped routes, copy the house guard verbatim:
16+
`if not user.is_admin and user.user_id != p["user_id"]` followed by a masked
17+
404 (see routes/projects.py). A bare 403 without the admin bypass both locks
18+
out admins and leaks resource existence to other users (#2042).
1519

1620
**2. Bind both directions on authenticated channels.**
1721
When a message or envelope arrives over an authenticated channel, verify BOTH
@@ -37,6 +41,16 @@ and strips it. Tokens we only VERIFY are stored hashed; tokens we must PRESENT
3741
outbound are the only ones stored recoverable. Example: PR #2009 moved the
3842
GitHub App RSA key out of plaintext config.yaml.
3943

44+
**16. Never commit runtime state or key material.**
45+
Anything a store or subsystem writes under `data/` at runtime is state, not
46+
source. A PR that introduces a component writing under `data/` must add that
47+
directory to `.gitignore` in the SAME PR, and `git status` must be checked for
48+
runtime artifacts before every commit. Any private key that reaches a pushed
49+
commit is burned: regenerate it, and keep it out of the target branch's history
50+
(squash merge, or rewrite the branch). Example: `data/hub/identity.json` with
51+
live signing/encryption keys entered one branch's history and was then
52+
re-committed by a second PR (#2043 history, #2042).
53+
4054
## Correctness
4155

4256
**5. Never take element `[0]` of a collection that can hold more than one.**
@@ -74,6 +88,12 @@ share flow), state it in the PR body and file a follow-up issue. Example: PR
7488
permissions. Wire the real value or do not add the parameter yet. Example:
7589
PR #2036 `handleSaveGrants`.
7690

91+
**17. A new view must be wired into every surface it has: desktop AND mobile.**
92+
The desktop tab list and the mobile tab order are separate registries; updating
93+
one and not the other ships a view that is unreachable on phones (#2042:
94+
`TABS` updated, `mobileTabOrder` missed). Grep for every registry the sibling
95+
views appear in and update all of them.
96+
7797
## Store and schema
7898

7999
**11. SCHEMA is the frozen v1; new columns and their indexes go in MIGRATIONS.**
@@ -87,6 +107,18 @@ Running a data migration twice must be a no-op (existence checks, INSERT OR
87107
IGNORE, migrated-from markers) and there must be a test proving the second run
88108
changes nothing. Example done right: PR #2028 `test_migrate_idempotent`.
89109

110+
**18. Retrofitting a column onto an already-shipped store needs a guarded
111+
ALTER, not just a MIGRATIONS entry.**
112+
The migration runner baselines pre-existing databases at the latest version
113+
WITHOUT executing the migrations (FOOTGUN #2 in `db_migrations.py`'s own
114+
docstring), so a plain `MIGRATIONS = [(1, "ALTER TABLE ...")]` on a store that
115+
already shipped is a silent no-op on every upgraded install: fresh DBs work,
116+
upgraded DBs lose the feature at runtime. Use the guarded `_post_init` pattern
117+
(PRAGMA table_info, ALTER only when the column is absent - see
118+
`knowledge_store._migration_v1_add_user_id`) and add an upgrade test that
119+
builds the PRE-change schema first. Fresh-DB tests are structurally blind to
120+
this class (#2043: `peer_fingerprint`).
121+
90122
## Process
91123

92124
**13. A fix and its test belong in one PR.**
@@ -110,3 +142,87 @@ Findings are gated on severity of content, not review state. Address each one
110142
(fix it or rebut it concretely in the thread); never merge past an open
111143
finding, and never let a "pass" verdict from a stale commit stand in for the
112144
current head.
145+
A finding is closed only when it is ANSWERED IN-THREAD: reply to each numbered
146+
item with the commit that addresses it or a concrete rebuttal. Pushing code
147+
without replies leaves the finding open - the reviewer re-verifies blind and
148+
the verdict stays HOLD (#2043 round two).
149+
150+
**19. State scope deltas against the issue explicitly.**
151+
If a PR ships less than its issue scopes - a subfeature dropped, owner-only
152+
routes where the issue says peer-serving, a "live" view that fetches once -
153+
say so in the PR body and file the follow-up issue in the same push. Never let
154+
a partial slice close the parent issue. Silent shortfalls read as done, get
155+
caught in review anyway, and cost a full extra round (#2042: community chat
156+
and peer access absent with no deferral note).
157+
158+
**20. A tolerance is not a test of an invariant.**
159+
An assertion like `assert abs(after - before) <= 2` cannot distinguish correct
160+
behaviour from catastrophic failure. In PR #2062 that exact assertion ran green
161+
while reprocess destroyed the user's original uploaded file: the observed values
162+
were `before=2, after=0`, which the tolerance accepted, and the same tolerance
163+
would equally have accepted a doubling to 4. If the property is "the count does
164+
not change", assert equality and assert the resulting status, so the test fails
165+
loudly on both loss and duplication. Reserve tolerances for genuinely
166+
approximate quantities such as timings, and even then bound them tightly.
167+
168+
**21. Shell snippets inside template literals must escape `${`.**
169+
A bash or PowerShell snippet stored in a JavaScript template literal collides
170+
with the language's own interpolation: `${VAR:-default}` is parsed as JS, not
171+
shell. In PR #2077 this produced 225 TypeScript syntax errors from a single
172+
cause, in one of four otherwise-clean files, and read like incoherent output
173+
rather than one mechanical mistake. Escape as `\${`, or keep snippets in plain
174+
non-template strings or separate asset files. The class generalises to any
175+
language sharing `${...}` with the shell, and it is invisible to anything that
176+
does not actually compile the file, which is why the frontend typecheck gate
177+
exists before a PR is opened.
178+
179+
**22. Conflict resolution is a decision, not a mechanical act.**
180+
Taking the wrong side of a hunk silently reverts fixes that were just made. When
181+
a base branch has moved substantially under a long-lived branch, read what
182+
changed underneath before resolving: the four defects fixed in Library P1
183+
(nested response envelope, the guard preventing reprocess from deleting the
184+
source upload, the compare-and-swap status transition, exact artifact-count
185+
assertions) are all reintroducible by a plausible-looking resolution. Rebase
186+
rather than merging the base in, so the diff stays reviewable and each conflict
187+
is seen individually.
188+
189+
**23. Mocks of EXTERNAL service contracts need a real source, not a guess.**
190+
Mocking our own internals or injecting errors you cannot produce on demand (a
191+
500, a timeout, an ImportError) is fine and unavoidable. The dangerous class is
192+
narrow: a mock of a service we do not control, whose fixture was hand-written
193+
from what the calling code expects. That fixture encodes a BELIEF about someone
194+
else's API, and when the belief is wrong the test proves nothing while going
195+
green. This happened three times in one PR cycle (#2062): an invented `dbPath`
196+
request shape, an un-nested poll response, and a tolerance assertion over both.
197+
198+
First, split the class by whether the contract has a reachable OWNER.
199+
200+
**Sibling services (taOSmd, taOS website): ASK. Do not guess.** These are not
201+
third parties. Their maintainer is on the A2A bus, and asking costs one message.
202+
Every failure in the #2062 cycle came from inferring a contract that was free to
203+
obtain: the builder guessed a request shape, and the reviewer verified against
204+
source rather than asking the owner. When we finally asked, we got the envelope
205+
documented, a wrong stats key list corrected by its own author, and a
206+
`/version` capabilities endpoint built to make the contract machine-checkable.
207+
Reverse-engineering a sibling's API from its source is a smell, not diligence:
208+
source tells you what it does today, the owner tells you what it guarantees.
209+
Contributors without bus access (external collaborators) ask in the PR, and the
210+
lead relays.
211+
212+
**Genuinely third-party (GitHub, OpenRouter, Reddit): capture, do not compose.**
213+
There is no one to ask, so call the real service once and commit its response as
214+
the fixture. A recorded response cannot encode a wrong belief.
215+
216+
For both, then:
217+
218+
1. **Keep one real integration test per external contract.** Have it feature
219+
detect and skip when the service is unreachable, so CI stays green offline
220+
but drift surfaces the moment anyone runs it against a live instance. For
221+
taosmd, `GET /version` returns a capabilities list for exactly this.
222+
2. **If that is impossible, mark the mock provisional IN CODE** with the
223+
contract source and the date it was verified. A follow-up issue is not
224+
sufficient: it detaches the caveat from the code and, in a tracker with
225+
hundreds of open items, functions as indefinite deferral.
226+
227+
Reviewer's job: ask where the fixture came from. A green test over a fictional
228+
contract is worse than no test, because it certifies the bug.

docs/getting-started.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Any of the following will work. More RAM means bigger, more capable models.
2222
|--------|-----|-------|
2323
| **Orange Pi 5 Plus** (recommended) | 16 GB | RK3588 chip with 6 TOPS NPU for fast inference |
2424
| **Orange Pi 5** | 8–16 GB | Same NPU, slightly fewer I/O ports |
25-
| **Raspberry Pi 5** | 8 GB minimum | CPU-only inference unless you add an accelerator HAT |
25+
| **Raspberry Pi 5** | 8 GB minimum | CPU-only inference unless you add an accelerator HAT; with the **AI HAT+2 (Hailo-10H, 40 TOPS)** you get NPU-accelerated LLM inference |
2626
| **Any x86/x64 PC or laptop** | 4 GB+ | Budget PC, old laptop, NUC, etc. GPU optional |
2727
| **NVIDIA GPU system** | 4 GB+ VRAM | GTX 1050 Ti and up; CUDA acceleration |
2828
| **AMD GPU system** | 8 GB+ VRAM | RX 6600 and up; ROCm acceleration |
@@ -31,6 +31,8 @@ The platform itself uses roughly 345 MB of RAM when idle, so it runs comfortably
3131

3232
**Not sure which to buy?** The Orange Pi 5 Plus with 16 GB is the recommended starting point. It has a built-in NPU (neural processing unit) that runs models significantly faster than the CPU alone, and 16 GB gives you room to run multiple agents at once.
3333

34+
> **AI HAT+2 (Hailo-10H) and the Pi 5 M.2 slot:** The AI HAT+2 NPU accelerator occupies the Raspberry Pi 5's only M.2 slot, the same slot normally used for an NVMe boot drive. You cannot use the HAT and an NVMe SSD at the same time. If you want fast storage alongside Hailo LLM acceleration, use a USB SSD instead.
35+
3436
### Software
3537

3638
- **OS:** Armbian or Debian-based Linux (Ubuntu works too). The installer handles everything else.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "tinyagentos"
7-
version = "1.0.0-beta.42"
7+
version = "1.0.0-beta.43"
88
description = "Self-hosted AI agent memory system for low-power hardware"
99
license = { file = "LICENSE" }
1010
# Upper-capped at <3.14 because litellm (the proxy extra, the agent/model proxy

tests/projects/test_invite_store.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,44 @@ async def test_revoke_nonexistent_returns_false(store):
315315
assert await store.revoke("000000") is False
316316

317317

318+
@pytest.mark.asyncio
319+
async def test_revoke_expired_invite(store):
320+
"""An expired invite can still be revoked (cleanup from the pending list)."""
321+
result = await store.mint(
322+
project_id="prj-1",
323+
scopes=[],
324+
approval_mode="auto",
325+
check_interval_secs=1800,
326+
created_by="u",
327+
)
328+
iid = result["record"]["invite_id"]
329+
await store._db.execute(
330+
"UPDATE project_invites SET expires_ts = 1 WHERE invite_id = ?", (iid,)
331+
)
332+
await store._db.commit()
333+
row = await store.get(iid)
334+
assert row["status"] == "expired"
335+
ok = await store.revoke(iid)
336+
assert ok is True
337+
row = await store.get(iid)
338+
assert row["status"] == "revoked"
339+
340+
341+
@pytest.mark.asyncio
342+
async def test_revoke_redeemed_invite_returns_false(store):
343+
result = await store.mint(
344+
project_id="prj-1",
345+
scopes=[],
346+
approval_mode="auto",
347+
check_interval_secs=1800,
348+
created_by="u",
349+
)
350+
iid = result["record"]["invite_id"]
351+
pin = result["pin"]
352+
await store.redeem(iid, pin)
353+
assert await store.revoke(iid) is False
354+
355+
318356
# ---------------------------------------------------------------------------
319357
# redeem
320358
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)