Skip to content
Merged

Dev #412

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
228 commits
Select commit Hold shift + click to select a range
6cd1790
test(frontend): add SideChatPanel unit tests
jlunder00 May 21, 2026
88646bf
feat(frontend): add SideChatPanel component
jlunder00 May 21, 2026
c065ab4
feat(bot): wire tether-agent-2.0 to interactive-agent-layer real pipe…
jlunder00 May 21, 2026
891fa30
test(frontend): add AppNav nav link and keyboard shortcut tests
jlunder00 May 21, 2026
1766b18
feat(frontend): wire trial counter, BYOK gate, and agent settings tog…
jlunder00 May 21, 2026
922e8b8
feat(frontend): add Chat nav link + Phase J side chat panel
jlunder00 May 21, 2026
c1ededa
Wire A6 control protocol into interactive-agent-layer session
jlunder00 May 21, 2026
9b415b2
refactor(frontend): collapse App.vue nav links into v-for
jlunder00 May 21, 2026
be66c14
refactor: simplify A6 control protocol test imports
jlunder00 May 21, 2026
a03538d
feat(bot): add event_fn streaming and fix LayerClient incremental SSE
jlunder00 May 21, 2026
3cf00e6
Merge pull request #392 from jlunder00/feature/trial-byok-frontend
jlunder00 May 21, 2026
769f094
Merge pull request #393 from jlunder00/feature/chat-nav-and-side-panel
jlunder00 May 21, 2026
f0b85e0
Emit permission_request on SSE stream instead of WSPublisher
jlunder00 May 21, 2026
91f19f8
Merge pull request #391 from jlunder00/feature/agent2-real-pipeline
jlunder00 May 21, 2026
f95b438
Merge pull request #394 from jlunder00/feature/layer-a6-wire-up
jlunder00 May 21, 2026
be446a7
feat(dispatch): wire tether-agent-2.5 to premium handler with is_paid…
jlunder00 May 21, 2026
76d7c32
fix(dispatch): admin users bypass subscription check for tether-agent…
jlunder00 May 21, 2026
94b2664
fix(tests): update stale dispatch tests for post-M3/M4 pipeline behavior
jlunder00 May 21, 2026
6cf01a7
Merge pull request #395 from jlunder00/feature/m4-premium-pipeline
jlunder00 May 21, 2026
d9abceb
chore: bump tether-premium pin to 0.0.2a9 (#396)
jlunder00 May 21, 2026
5c65894
chore: bump cloud version to 0.4.0a1
jlunder00 May 21, 2026
2fd92d4
fix: wire real PoolClient in layer __main__ (StubPoolClient removed i…
jlunder00 May 21, 2026
709754e
Merge pull request #397 from jlunder00/fix/premium-pin-workflow
jlunder00 May 21, 2026
f2ffb05
chore: bump cloud version to 0.4.1a1
jlunder00 May 21, 2026
103c3b7
fix(settings): remove extra user_id arg from set_user_setting call
jlunder00 May 21, 2026
a619bea
Merge pull request #398 from jlunder00/fix/settings-put-arg-count
jlunder00 May 21, 2026
7add44e
fix(picker): open dropdown upward + load preference on ConversationVi…
jlunder00 May 21, 2026
7da10a7
fix(bot): emit turn_error SSE event on layer run_turn failure
jlunder00 May 21, 2026
de53608
Merge pull request #399 from jlunder00/fix/picker-scroll-and-reset
jlunder00 May 21, 2026
a47d70e
Merge pull request #400 from jlunder00/fix/layer-fallback-investigation
jlunder00 May 21, 2026
8a07c4c
chore: bump cloud version to 0.4.2a1
jlunder00 May 21, 2026
fe9efe6
feat: pool observability — structured logging, /metrics endpoint, tok…
jlunder00 May 22, 2026
281b3eb
fix: correct histogram cumulative rendering and harden token-write ta…
jlunder00 May 22, 2026
4a1657f
feat: Redis pub/sub for cross-process event delivery
jlunder00 May 22, 2026
4eaa608
test+feat: add assignNode action to conversations store
jlunder00 May 22, 2026
40e30ba
test+feat: add ContextNodeSidebar component
jlunder00 May 22, 2026
9d24529
test+feat: add activeNodeId prop and drag support to ConversationList
jlunder00 May 22, 2026
c8aa8d5
feat: add context node sidebar to chat page
jlunder00 May 22, 2026
c789306
fix: show expand chevron optimistically when children_count is absent
jlunder00 May 22, 2026
6bc007a
fix: compose both filters in ConversationList and assignNode in-place…
jlunder00 May 22, 2026
717ec88
fix: address code review findings on Redis pub/sub
jlunder00 May 22, 2026
9bbfb13
docs: add interactive-agent-layer architecture notes
jlunder00 May 22, 2026
fabd44a
fix(bot): prevent double-send when premium handler streams then raises
jlunder00 May 22, 2026
4b8d52b
Merge pull request #402 from jlunder00/feature/pool-observability-and…
jlunder00 May 22, 2026
d6b8983
Merge pull request #403 from jlunder00/feature/context-node-sidebar
jlunder00 May 22, 2026
6b9121e
Merge pull request #404 from jlunder00/feature/redis-pubsub-cross-pro…
jlunder00 May 22, 2026
96fae7a
Merge pull request #405 from jlunder00/docs/layer-spec-amendment
jlunder00 May 22, 2026
14cf15f
Merge pull request #406 from jlunder00/fix/m4-bug2-double-send
jlunder00 May 22, 2026
f05c8ef
chore: bump cloud version to 0.5.0a1
jlunder00 May 22, 2026
f838cda
fix(deploy): add redis-server binary and redis Python package to image
jlunder00 May 22, 2026
d9a5643
Merge pull request #407 from jlunder00/fix/redis-deploy-deps
jlunder00 May 22, 2026
5db1e13
chore: bump cloud version to 0.5.1a1
jlunder00 May 22, 2026
0a0a150
feat(frontend): add Beacon priority styling to system messages
jlunder00 May 22, 2026
fa6c259
feat(frontend): add Beacon priority styling to system messages
jlunder00 May 22, 2026
4724c3e
feat(frontend): add Beacon suppression history view skeleton
jlunder00 May 22, 2026
a049ddf
fix(frontend): fix broken suppressions 404 test + a11y sr-only fix
jlunder00 May 22, 2026
1795ed5
Merge pull request #408 from jlunder00/feature/beacon-fe-prep
jlunder00 May 22, 2026
886f35f
chore: bump cloud version to 0.5.2a1
jlunder00 May 22, 2026
7c9ed40
test(router): add K1 child route + redirect tests
jlunder00 May 22, 2026
0d0ba30
feat(chat): add FolderCenterPanel with tests
jlunder00 May 22, 2026
0cb3b49
feat(chat): add ProjectDetailsPanel with autosave + tests
jlunder00 May 22, 2026
7a3911c
feat(chat): ContextNodeSidebar collapse + conv leaves + new chat
jlunder00 May 22, 2026
533fa70
feat(chat): rewrite ChatPageView with mode-switching + collapse
jlunder00 May 23, 2026
80c831c
feat(frontend+api): notification routing settings UI (Beacon Wave 6 D1)
jlunder00 May 23, 2026
a5ac78e
feat(frontend): @handle mention autocomplete in chat composer (Beacon…
jlunder00 May 23, 2026
e9f1b04
fix(bot,db): tc.created_at column error + premium handler exc_info lo…
jlunder00 May 23, 2026
ae1097a
feat(chat): K1 layout refactor — folder mode + conversation mode
jlunder00 May 23, 2026
90ae268
fix(frontend): remove unused flushPromises import in mentions test
jlunder00 May 23, 2026
8aa5866
Merge pull request #409 from jlunder00/feature/beacon-fe-wave6-routin…
jlunder00 May 23, 2026
0e1b56c
Merge pull request #410 from jlunder00/fix/wave6-bot-and-premium-bugs
jlunder00 May 23, 2026
908b896
fix(chat): K1 code review findings
jlunder00 May 23, 2026
4b90a04
Merge pull request #411 from jlunder00/feature/phase-k1-folder-editing
jlunder00 May 23, 2026
fa0c38f
chore: bump cloud version to 0.6.0a1
jlunder00 May 23, 2026
65cbda1
feat(frontend): K1-PR2 sidebar editing — rename, create, delete, fold…
jlunder00 May 23, 2026
9b0da2e
feat: add pool warm hint on chat page load
jlunder00 May 23, 2026
de763da
fix: cache PoolClient on app.state for connection reuse
jlunder00 May 23, 2026
f7ab6a3
feat(frontend): Beacon wave 7 — conversation state actions + suppress…
jlunder00 May 23, 2026
32453be
fix(frontend): address code-reviewer findings on K1-PR2 sidebar editing
jlunder00 May 23, 2026
b531c68
feat(db): Beacon Phase 3 prep — memory tables + conversation lifecycl…
jlunder00 May 23, 2026
9cdad88
fix(frontend): extend ConversationStateActions overflow to closed sta…
jlunder00 May 23, 2026
28b5873
test(db): add RLS write-side smoke test for beacon_memory
jlunder00 May 23, 2026
06078a1
Merge pull request #415 from jlunder00/feature/beacon-fe-wave7-actions
jlunder00 May 23, 2026
a5d6fa4
Merge pull request #414 from jlunder00/feature/phase-k1-pr2-sidebar-e…
jlunder00 May 23, 2026
e119c8f
Merge pull request #413 from jlunder00/feature/pool-warming
jlunder00 May 23, 2026
6db7e47
chore: bump tether-premium pin to 0.1.1a1 (#417)
jlunder00 May 23, 2026
d5b24a4
fix(db): remove now() from beacon_suppressions partial index predicate
jlunder00 May 23, 2026
487e6ab
Merge pull request #416 from jlunder00/feature/beacon-phase3-prep
jlunder00 May 23, 2026
2ef20f2
chore: bump cloud version to 0.6.1a1
jlunder00 May 23, 2026
78d76d7
add session_notes table and pg_queries layer
jlunder00 May 24, 2026
fb762b5
fix(pool): auth propagation and zombie subprocess cleanup
jlunder00 May 24, 2026
9d36b09
tighten updated_at assertion in test_session_notes
jlunder00 May 24, 2026
8b9d6f7
revert updated_at assertion to >= (won't false-fail)
jlunder00 May 24, 2026
0552c2e
fix(pool): auth propagation and zombie subprocess cleanup
jlunder00 May 24, 2026
2b6d7ea
Merge pull request #418 from jlunder00/feature/session-notes-db-public
jlunder00 May 24, 2026
e0d22e5
Merge pull request #419 from jlunder00/fix/pool-warm-auth-and-cleanup
jlunder00 May 24, 2026
9383122
chore: bump tether-premium pin to 0.1.1a2 (#420)
jlunder00 May 24, 2026
8a47b68
chore: bump cloud version to 0.6.2a1
jlunder00 May 24, 2026
ebceae7
fix(pool): refuse spawns without CLAUDE_CODE_OAUTH_TOKEN (spawn guard)
jlunder00 May 24, 2026
6697ae8
feat(pool): add spawn_guard_rejection_total metric
jlunder00 May 24, 2026
871c5b2
Merge pull request #421 from jlunder00/fix/pool-warm-refill-auth-gap
jlunder00 May 24, 2026
2cb42cb
chore: bump cloud version to 0.6.3a1
jlunder00 May 24, 2026
4ef01a2
diag(pool): instrument warm spawn path with INFO-level visibility logs
jlunder00 May 24, 2026
fdde02e
fix(pool): use dataclasses.fields() instead of dir() to enumerate SDK…
jlunder00 May 24, 2026
badcced
Merge pull request #422 from jlunder00/fix/pool-warm-diagnostic-logging
jlunder00 May 24, 2026
f07d572
chore: bump cloud version to 0.6.4a1
jlunder00 May 24, 2026
d545d3e
fix(pool): expand mcp_servers list placeholder into real SSE config a…
jlunder00 May 24, 2026
6d7230f
fix(pool): address code-reviewer findings — cross-user isolation, key…
jlunder00 May 24, 2026
e970e08
Merge pull request #423 from jlunder00/fix/pool-warm-mcp-shape
jlunder00 May 24, 2026
e90338c
chore: bump cloud version to 0.6.5a1
jlunder00 May 24, 2026
19205e4
fix: pool MCP fallback and empty user_id production bugs
jlunder00 May 24, 2026
54c587d
Merge pull request #424 from jlunder00/fix/pool-warm-mcp-shape
jlunder00 May 24, 2026
3008526
chore: bump cloud version to 0.6.6a1
jlunder00 May 24, 2026
114c6e6
add hosts entry line
jlunder00 May 24, 2026
51563a6
chore: bump cloud version to 0.6.6a2
jlunder00 May 24, 2026
c34f738
bump anthropic token setup timeout
jlunder00 May 25, 2026
645e238
chore: bump cloud version to 0.6.6a3
jlunder00 May 25, 2026
83fc4a7
Merge branch 'main' of github.com:jlunder00/tether into fix/premium-p…
jlunder00 May 25, 2026
13f389a
Merge branch 'dev' of github.com:jlunder00/tether into fix/premium-pi…
jlunder00 May 25, 2026
f26f077
feat(bot): wire 2.5 pipeline through pool manager
jlunder00 May 25, 2026
040e611
fix(bot): address pipeline-pool code review findings
jlunder00 May 25, 2026
64163a1
fix: reject empty user_id at API boundary in pool manager
jlunder00 May 25, 2026
3b6ff43
feat: add POST /setup-token to agent pool manager
jlunder00 May 25, 2026
bb1f555
feat: proxy anthropic setup-token through pool manager
jlunder00 May 25, 2026
2dd4f60
fix: address code review findings on pool-unify setup-token
jlunder00 May 25, 2026
d6fb08a
Merge pull request #426 from jlunder00/feature/pool-unify
jlunder00 May 25, 2026
f8253b5
Merge pull request #425 from jlunder00/feature/pipeline-pool
jlunder00 May 25, 2026
ad1c71d
chore: bump cloud version to 0.6.7a1
jlunder00 May 25, 2026
f5ac9af
chore: bump tether-premium pin to 0.1.1a3 (#427)
jlunder00 May 25, 2026
b03d1be
fix: normalise empty user_id to None in refill run_once
jlunder00 May 25, 2026
e942553
fix: use get_conn for RLS-scoped api_keys create/revoke in pool manager
jlunder00 May 25, 2026
f3d9be3
Merge pull request #428 from jlunder00/feature/pool-rls-fix
jlunder00 May 25, 2026
a402774
chore: bump cloud version to 0.6.8a1
jlunder00 May 25, 2026
149dcd5
feat: configurable initialize timeout (CLAUDE_CODE_STREAM_CLOSE_TIMEOUT)
jlunder00 May 25, 2026
afee354
feat: home directory pool — per-subprocess isolated $HOME
jlunder00 May 25, 2026
7d81131
feat: seed claude-home-template in Dockerfile build step
jlunder00 May 25, 2026
06af058
fix: address code-reviewer findings in HomeDirPool
jlunder00 May 25, 2026
e36df18
Merge pull request #429 from jlunder00/feature/pool-home-dirs
jlunder00 May 25, 2026
4e69328
chore: bump cloud version to 0.6.9a1
jlunder00 May 25, 2026
83e9627
fix: use full Anthropic API model ID for haiku in pool spawn options
jlunder00 May 25, 2026
4c85c2a
chore: normalize haiku model ID in test fixtures to claude-haiku-4-5-…
jlunder00 May 25, 2026
347a348
fix(bot): correct WebSocket event types — chunk/done → turn_complete/…
jlunder00 May 25, 2026
c5ff20f
Merge pull request #430 from jlunder00/feature/pool-model-fix
jlunder00 May 25, 2026
8a3c6fd
fix(bot): address code-review findings — error paths + session_id field
jlunder00 May 25, 2026
fa5933f
fix(tests): prevent CI hangs in test_ws_event_types.py
jlunder00 May 25, 2026
2d07f64
fix(tests): update test_agent_dispatch_ws.py for new WS protocol
jlunder00 May 25, 2026
9821479
Merge pull request #431 from jlunder00/feature/ws-event-type-fix
jlunder00 May 25, 2026
a871271
chore: bump cloud version to 0.6.10a1
jlunder00 May 25, 2026
3aec58f
fix: set CLAUDE_CODE_STREAM_CLOSE_TIMEOUT in os.environ and wire Home…
jlunder00 May 25, 2026
a6c6c08
test: add monkeypatch cleanup for CLAUDE_CODE_STREAM_CLOSE_TIMEOUT in…
jlunder00 May 25, 2026
589a055
bot: always call tracked_send even for falsy premium handler response
jlunder00 May 25, 2026
e1bf7be
Merge pull request #432 from jlunder00/feature/pool-env-fix
jlunder00 May 25, 2026
a046ce7
Merge pull request #433 from jlunder00/feature/ws-tracked-send-fix
jlunder00 May 25, 2026
d95b1a1
chore: bump cloud version to 0.6.11a1
jlunder00 May 25, 2026
2149d2f
fix(pool): create homes dir, seed template .claude.json, bump timeout
jlunder00 May 25, 2026
76ab17e
fix(conversations): align messages API shape with frontend MessagesPa…
jlunder00 May 25, 2026
ce010fe
fix(conversations): address code review — warn on JSON parse failure,…
jlunder00 May 25, 2026
9bb1d51
Merge pull request #435 from jlunder00/feature/frontend-json-fix
jlunder00 May 25, 2026
7f81685
Merge pull request #434 from jlunder00/feature/infra-pool-dirs
jlunder00 May 25, 2026
a8e73a9
chore: bump cloud version to 0.6.12a1
jlunder00 May 25, 2026
d88e6c9
fix(bot): add delivery log to dispatch_v25 and WS turn_complete
jlunder00 May 25, 2026
60a318a
Merge pull request #436 from jlunder00/feature/bot-response-fix
jlunder00 May 26, 2026
8797a8d
chore: bump cloud version to 0.6.13a1
jlunder00 May 26, 2026
a24fe32
chore: bump tether-premium pin to 0.1.1a4 (#437)
jlunder00 May 26, 2026
d2b6f1d
fix(chat): capture turn_complete.final_text in useConversationChat
jlunder00 May 26, 2026
130cdde
Merge pull request #438 from jlunder00/feature/frontend-ws-render-fix
jlunder00 May 26, 2026
2762d95
chore: bump cloud version to 0.6.14a1
jlunder00 May 26, 2026
2bc600e
fix: remove silent inline fallback in PipelineBackend
jlunder00 May 26, 2026
abad811
feat: add llm.layer_base_url to app_config.yaml
jlunder00 May 26, 2026
c70b34d
test: add Fix 3 tests for PipelineBackend no-inline-fallback
jlunder00 May 26, 2026
851bb18
Update test_pipeline_pool tests to match Fix 3 propagation behavior
jlunder00 May 26, 2026
edb653e
Merge pull request #439 from jlunder00/feature/pool-wiring-fix
jlunder00 May 26, 2026
4acd4fb
chore: bump cloud version to 0.6.15a1
jlunder00 May 26, 2026
fe074be
chore: bump tether-premium pin to 0.1.1a5 (#440)
jlunder00 May 26, 2026
1d441f7
hotfix: fix hanging redis pubsub cancellation test in Python 3.11
jlunder00 Jun 4, 2026
8cbe864
hotfix: fix redis pubsub listen() cancellation hang in Python 3.11
jlunder00 Jun 4, 2026
a038b01
hotfix: replace pubsub.listen() with get_message polling loop
jlunder00 Jun 4, 2026
05ad1b9
hotfix: add timeout guards to finally-block cleanup
jlunder00 Jun 4, 2026
792d332
Fix subscribe_and_forward hanging on task cancellation in Python 3.11
jlunder00 Jun 4, 2026
debfc90
Merge pull request #445 from jlunder00/hotfix/redis-test-hang
jlunder00 Jun 4, 2026
0b3080d
feat(frontend): Stream E — index-based tree + optimistic create
jlunder00 Jun 3, 2026
a32c3ab
fix(frontend): address refreshForNode gap + clean up conversations store
jlunder00 Jun 3, 2026
a43c89b
feat(frontend): consume state+priority from conversations/index response
jlunder00 Jun 3, 2026
5061f21
feat: memory-context v2 schema + MCP tools (Stream A foundations)
jlunder00 Jun 3, 2026
aac4d9f
fix: move all SQL out of tool layer; fix scope-bypass in recursive read
jlunder00 Jun 3, 2026
7198920
fix: rename migration revision k1l2m3n4o5p6 → l1m2n3o4p5q6 (collision…
jlunder00 Jun 3, 2026
cc1c510
Add lightweight conversation and node index endpoints
jlunder00 Jun 3, 2026
e5c1f1d
Add nodes RLS isolation test; strengthen route-ordering guards
jlunder00 Jun 3, 2026
1ed52b4
Add state + priority to conversations/index response
jlunder00 Jun 3, 2026
58962d3
feat(layer): migrate event schemas to Stream B spec (agent_action, pe…
jlunder00 Jun 3, 2026
bbc7257
fix(layer/frontend): handle interrupted event in chat store to clear …
jlunder00 Jun 3, 2026
62fc6c5
fix: migrate PermissionModal to new permission_request schema
jlunder00 Jun 4, 2026
37cd64a
test: add pytest-timeout to dev deps + 60s caps on new event schema t…
jlunder00 Jun 4, 2026
64a1a81
fix: clean up seeded schema-test users after test_memory_context_schema
jlunder00 Jun 4, 2026
a51a77a
fix: add explicit user_id filter to list_nodes_index for cross-user i…
jlunder00 Jun 4, 2026
b5631e8
fix: use sync module fixture for schema-test user cleanup
jlunder00 Jun 4, 2026
e8b0d5b
Merge pull request #441 from jlunder00/feature/conversation-index
jlunder00 Jun 4, 2026
8e86dfa
Merge pull request #442 from jlunder00/feature/frontend-tree-optimistic
jlunder00 Jun 4, 2026
d227095
Merge pull request #443 from jlunder00/feature/db-mcp-foundations
jlunder00 Jun 4, 2026
5fa8781
Merge pull request #444 from jlunder00/feature/layer-events-emit
jlunder00 Jun 4, 2026
c1fb1b6
db: merge alembic heads from stream A and stream B
jlunder00 Jun 4, 2026
9217d30
Merge pull request #446 from jlunder00/feature/alembic-merge-heads
jlunder00 Jun 4, 2026
ec43af5
feat(store): add activeActions, currentPhase, fix permission_response…
jlunder00 Jun 4, 2026
5cf78b3
test(frontend): fix usePoolWarm Vitest — wrap composable in component…
jlunder00 Jun 4, 2026
500b996
feat(components): add AgentActionPill and StatusIndicator
jlunder00 Jun 4, 2026
cbb34dc
feat(ui): wire AgentActionPill, StatusIndicator into BotChat, block i…
jlunder00 Jun 4, 2026
120a54f
feat(mcp): harden write_node_memory + read_context enforcement (v2)
jlunder00 Jun 4, 2026
4e02b87
feat(mcp): implement search_context and search_memory tools
jlunder00 Jun 4, 2026
decb958
fix(mcp): add scope whitelist guard to search_user_memory
jlunder00 Jun 4, 2026
7612517
Merge pull request #447 from jlunder00/feature/pool-warm-fix
jlunder00 Jun 13, 2026
8cba294
Merge pull request #448 from jlunder00/feature/events-renderer
jlunder00 Jun 13, 2026
257e4f1
Merge pull request #449 from jlunder00/feature/memory-hardening
jlunder00 Jun 13, 2026
daba3df
Merge pull request #450 from jlunder00/feature/search-impl
jlunder00 Jun 13, 2026
86d85b9
feat(layer): wire check_grant_fn / insert_grant_fn through Layer.__in…
jlunder00 Jun 13, 2026
9e615c0
Merge pull request #451 from jlunder00/feature/layer-grant-wiring
jlunder00 Jun 13, 2026
a71f5f1
chore: bump cloud version to 0.7.0a1
jlunder00 Jun 13, 2026
360a2a6
chore: bump tether-premium pin to 0.1.1a6 (#452)
jlunder00 Jun 13, 2026
1b5477f
chore: bump cloud version to 0.7.0a2
jlunder00 Jun 13, 2026
a9bda09
feat(mcp): M-level cascade depth gating in read_context + CoalescingB…
jlunder00 Jun 13, 2026
277aa4c
feat: permission_request timeout with session_timeout event and front…
jlunder00 Jun 13, 2026
d4c4574
Merge pull request #453 from jlunder00/feature/descent-gating
jlunder00 Jul 3, 2026
832f6c9
Merge pull request #454 from jlunder00/feature/permission-timeout
jlunder00 Jul 3, 2026
cc4cc15
feat(permissions): add read_out_of_scope scope gating to PermissionGate
jlunder00 Jun 13, 2026
fd56662
Merge pull request #455 from jlunder00/feature/scope-gating
jlunder00 Jul 3, 2026
c2d3769
fix(deps): bump dompurify to 3.4.11 for XSS advisory patches
jlunder00 Jul 3, 2026
e3c548d
fix(db): pool min_size=0 + idle lifetime so managed PG can scale to zero
jlunder00 Jul 3, 2026
6446b16
Merge pull request #461 from jlunder00/feature/frontend-dompurify
jlunder00 Jul 3, 2026
2f11f36
Merge pull request #462 from jlunder00/feature/infra-db-webhook
jlunder00 Jul 3, 2026
65df250
chore: bump cloud version to 0.7.0a3
jlunder00 Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ jobs:
tests/db/test_pg_sections.py \
tests/db/test_pg_auth_queries.py \
tests/db/test_pg_integrations.py \
tests/db/test_memory_context_schema.py \
tests/api/ \
-v

Expand Down
26 changes: 25 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ FROM python:3.11-slim
# cron is needed for anchor trigger scheduling in the bot container.
# curl is needed for the NodeSource setup script.
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libffi-dev cron git gosu curl \
gcc libffi-dev cron git gosu curl redis-server \
&& rm -rf /var/lib/apt/lists/*

# Install Node.js 20 via NodeSource so npm has a properly self-contained
Expand All @@ -34,6 +34,19 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
ARG CLAUDE_CODE_VERSION=2.1.116
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}

# Home directory template for the agent pool manager.
# Running `claude --version` once causes the CLI to write its initial config
# (~/.claude.json, ~/.claude/) into the temp home. We snapshot that state so
# each warm subprocess gets a pre-seeded home dir, eliminating first-run setup
# time and ~/.claude.json lock contention on concurrent spawns.
# If the CLI produces no files (e.g. newer version skips auto-init), the
# template dir is left empty — the isolation benefit still applies.
RUN HOME=/tmp/claude-template \
claude --version 2>/dev/null || true \
&& mkdir -p /etc/claude-home-template \
&& cp -r /tmp/claude-template/. /etc/claude-home-template/ 2>/dev/null || true \
&& chmod -R a+rX /etc/claude-home-template

# Non-root user for running services. UID 1000 matches the default Pi user
# so bind-mounted /data files are accessible without permission issues.
RUN useradd -m -u 1000 -s /bin/bash tether
Expand All @@ -45,6 +58,17 @@ RUN mkdir -p /run/tether/creds \
&& chown tether:tether /run/tether/creds \
&& chmod 0700 /run/tether/creds

# Agent pool manager home directory base.
# /var/lib/ is root-owned — the tether user cannot mkdir here at runtime,
# so the pool base dir must be pre-created and handed off in the image.
# Also hand ownership of the Claude home template to tether so that
# initialize() can write .claude.json into it at first boot (auth seeding).
RUN mkdir -p /var/lib/tether/claude-homes \
&& chown tether:tether /var/lib/tether/claude-homes \
&& chmod 755 /var/lib/tether/claude-homes \
&& chown tether:tether /etc/claude-home-template \
&& chmod 755 /etc/claude-home-template

WORKDIR /app

# External dependencies (cached layer — only reruns when requirements.txt changes)
Expand Down
17 changes: 17 additions & 0 deletions agent_pool_manager/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ class AgentPoolConfig:
control_response_timeout_seconds: float = 60.0
"""Seconds the pool waits for a control_response before denying the tool call."""

connect_timeout_seconds: float = 15.0
"""Max seconds to wait for ClaudeSDKClient.connect() before treating the spawn
as failed. Must be shorter than prime_timeout_seconds so a hung transport
is detected and killed before the priming phase would time out anyway."""

initialize_timeout_ms: int = 120000
"""Value injected as CLAUDE_CODE_STREAM_CLOSE_TIMEOUT (ms) in subprocess env.
The SDK uses this as its internal initialize timeout (floor: 60 000 ms).
Default: 120 000 ms (2 min)."""

home_dir_base: str = "/var/lib/tether/claude-homes"
"""Directory where isolated per-subprocess home dirs are created."""

home_dir_template: str = "/etc/claude-home-template"
"""Directory copied into each home dir at initialization.
If it does not exist, home dirs start empty (lock-contention fix still applies)."""


_FIELD_NAMES = {f.name for f in fields(AgentPoolConfig)}

Expand Down
172 changes: 172 additions & 0 deletions agent_pool_manager/homes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""Isolated per-subprocess home directory pool.

Each subprocess gets its own ``$HOME`` to eliminate ``~/.claude.json`` lock
contention when two subprocesses spawn simultaneously. Dirs are pre-created
at pool startup and reset to a template state before each assignment so stale
CLI state from a previous subprocess never bleeds into the next one.

Design notes:
- Dirs are never deleted during normal operation — only reset.
- Reset (``shutil.rmtree`` + ``shutil.copytree``) runs in a thread-pool
executor so it does not block the asyncio event loop during spawn.
- TTL sweep evicts stale assignments (e.g. a subprocess that was lost
without going through ``_terminate``).
"""
from __future__ import annotations

import asyncio
import logging
import shutil
import time
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from agent_pool_manager.config import AgentPoolConfig

log = logging.getLogger(__name__)

# Path to the running container user's .claude.json. Isolated to a constant
# so tests can monkeypatch without touching the filesystem at /home/tether.
_AUTH_SOURCE_PATH: Path = Path("/home/tether/.claude.json")


def _reset_home(home: Path, template: Path | None) -> None:
"""Synchronous reset called from a thread executor.

Removes all content in *home* then either copies *template* in (if it
exists) or leaves *home* as an empty directory.
"""
# Wipe and recreate so the dir is empty/fresh regardless of template.
shutil.rmtree(home, ignore_errors=True)
home.mkdir(parents=True, exist_ok=True)
if template is not None and template.is_dir():
# copytree requires the destination not to exist; use dirs_exist_ok
# (Python 3.8+) so we can copy *into* the already-created home dir.
shutil.copytree(str(template), str(home), dirs_exist_ok=True)


class HomeDirPool:
"""Pool of isolated home directories for agent subprocesses.

Call ``await initialize()`` once before first use.
Call ``await acquire()`` to obtain a home dir for a new subprocess.
Call ``release(path)`` when the subprocess terminates.
Call ``await sweep()`` periodically (from RefillLoop.run_once) to evict
stale assignments that were never released.
"""

def __init__(self, config: AgentPoolConfig) -> None:
self._config = config
self._base = Path(config.home_dir_base)
template_path = Path(config.home_dir_template)
self._template: Path | None = template_path if template_path.is_dir() else None
self._dirs: list[Path] = []
self._available: asyncio.Queue[Path] = asyncio.Queue()
# str(path) → expiry timestamp (time.monotonic)
self._checked_out: dict[str, float] = {}

async def initialize(self) -> None:
"""Create home dirs and seed from template. Idempotent on re-call."""
count = self._config.capacity_total
self._base.mkdir(parents=True, exist_ok=True)

# Re-discover template now (it may have been created since __init__)
template_path = Path(self._config.home_dir_template)
self._template = template_path if template_path.is_dir() else None

# Seed template with auth credentials at runtime if not already present.
# /home/tether/.claude.json is written by `claude setup-token` after
# the container first boots — it is never baked into the image (Fly
# secret). Copying it into the template here means every warm
# subprocess starts with valid credentials without an extra setup step.
# Skips gracefully when: template dir absent, source absent, or
# template already has its own .claude.json.
if self._template is not None:
template_claude_json = self._template / ".claude.json"
if not template_claude_json.exists() and _AUTH_SOURCE_PATH.exists():
try:
shutil.copy2(_AUTH_SOURCE_PATH, template_claude_json)
log.info(
"home_pool.seeded_template source=%s dest=%s",
_AUTH_SOURCE_PATH,
template_claude_json,
)
except OSError:
log.warning(
"home_pool.seed_failed source=%s dest=%s",
_AUTH_SOURCE_PATH,
template_claude_json,
exc_info=True,
)

for i in range(count):
home = self._base / f"home-{i}"
home.mkdir(parents=True, exist_ok=True)
# Skip reset for dirs currently checked out by a live subprocess —
# wiping them would corrupt the running CLI process's HOME.
if str(home) not in self._checked_out:
# Seed from template in executor to avoid blocking the loop.
await asyncio.to_thread(_reset_home, home, self._template)
if home not in self._dirs:
self._dirs.append(home)
# Populate the available queue; clear first to avoid duplication on re-init.

# Rebuild _available from dirs not currently checked out.
while not self._available.empty():
try:
self._available.get_nowait()
except asyncio.QueueEmpty:
break
checked_out_paths = set(self._checked_out.keys())
for home in self._dirs:
if str(home) not in checked_out_paths:
self._available.put_nowait(home)

log.info(
"home_pool.initialized count=%d template=%s available=%d",
count,
self._template,
self._available.qsize(),
)

async def acquire(self) -> Path:
"""Return a home dir, reset to template state.

Blocks (up to the caller's timeout) if the pool is exhausted.
Raises ``asyncio.QueueEmpty`` if called with nowait and no dirs are
available — callers should use ``asyncio.wait_for`` for timeouts.
"""
home = await self._available.get()
await asyncio.to_thread(_reset_home, home, self._template)
# Use 2× subprocess max_age as home TTL so sweep never evicts a home
# whose subprocess might still be running (drain-on-touch retirement
# means a subprocess can live past its nominal max_age).
expiry = time.monotonic() + self._config.max_age_seconds * 2
self._checked_out[str(home)] = expiry
log.debug("home_pool.acquired path=%s", home)
return home

def release(self, home: Path) -> None:
"""Return *home* to the available queue. Idempotent."""
key = str(home)
if key not in self._checked_out:
# Already released or never acquired — silently ignore.
return
self._checked_out.pop(key)
self._available.put_nowait(home)
log.debug("home_pool.released path=%s", home)

async def sweep(self) -> None:
"""Evict stale assignments that were never released (TTL-based safety net)."""
now = time.monotonic()
evicted = [k for k, expiry in list(self._checked_out.items()) if now > expiry]
for key in evicted:
self._checked_out.pop(key, None)
path = Path(key)
self._available.put_nowait(path)
log.warning("home_pool.sweep_evict path=%s", path)

def available_count(self) -> int:
"""Number of home dirs currently available (not checked out)."""
return self._available.qsize()
Loading
Loading