Skip to content

Release: beta → main (SDK >=0.1.68 + carry-forward fixes) - #322

Merged
olegbrok merged 22 commits into
mainfrom
beta
Apr 25, 2026
Merged

Release: beta → main (SDK >=0.1.68 + carry-forward fixes)#322
olegbrok merged 22 commits into
mainfrom
beta

Conversation

@olegbrok

Copy link
Copy Markdown
Collaborator

Summary

  • Bumps claude-agent-sdk minimum from >=0.1.66>=0.1.68
  • Picks up CLI v2.1.119 in the latest SDK
  • Brings beta even with main (merge of main into beta to avoid regressing the prior >=0.1.66 bump)

SDK version notes

Version CLI Notable
0.1.68 2.1.119 Internal-only (rebundle of CLI)
0.1.67 2.1.120 Trio-compat fixRuntimeError: no running event loop regression from v0.1.51. sniffio-based dispatch. Adds explicit sniffio runtime dep.
0.1.66 2.1.119 Already merged on main as #320 yesterday

What's in beta that isn't in main yet

Beta-only commits going to main with this PR (15 fixes from the prior bug sweep + presence + dep bumps + GPT-5.5 seed):

Plus the merge commit 5dcda4a (main → beta) and 4206933 (the SDK bump itself).

CI

✅ CI run 24928757278 — green (Lint, Test 3.11/3.12/3.13, Frontend, CodeQL)
✅ CodeQL run 24928757277 — green

Test plan

  • Merge to main (squash)
  • On Mac Mini: update_and_restart() to pull v0.1.68 + restart daemon
  • Verify claude_agent_sdk.__version__ == '0.1.68' after restart
  • Smoke test: barsik responds to a heartbeat, sends one message

Note on commit author

The 4206933 chore commit landed under Oleg <oleg@Olegs-Mac-mini.local> because Barsik's session has no global git identity set on the daemon host. Brad: this is fine, leaving as-is. Future Barsik commits will continue under this identity unless you set a different one.

🤖 Opened by Barsik

Co-Authored-By: Claude noreply@anthropic.com

dependabot Bot and others added 22 commits April 19, 2026 02:12
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the npm-minor-patch group in /frontend-svelte with 3 updates: [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router), [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `svelte-spa-router` from 5.0.0 to 5.0.1
- [Release notes](https://github.com/ItalyPaleAle/svelte-spa-router/releases)
- [Changelog](https://github.com/ItalyPaleAle/svelte-spa-router/blob/main/CHANGELOG.md)
- [Commits](ItalyPaleAle/svelte-spa-router@v5.0.0...v5.0.1)

Updates `svelte` from 5.55.0 to 5.55.4
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.55.4/packages/svelte)

Updates `vite` from 8.0.3 to 8.0.8
- [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.0.8/packages/vite)

---
updated-dependencies:
- dependency-name: svelte-spa-router
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor-patch
- dependency-name: svelte
  dependency-version: 5.55.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor-patch
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(presence): server-side last_seen stamping for non-CC agents (#279)

Non-CC agents (Codex, GLM) never call the send_heartbeat MCP tool, so
their presence showed stale despite active streaming sessions. Stamp
agents.last_seen_at server-side on every evidence of a working pipe:
broker delivery (inbound + inter-agent), and turn completion/error in
both StreamingSession and CodexSession.

- Add last_seen_at column + Agent.last_seen_at field + stamp_last_seen()
- _agent_presence takes max(server_ts, heartbeat_ts) — heartbeat path
  preserved so CC agents keep working unchanged
- Plumb AgentRegistry into StreamingSession/CodexSession via new
  `registry` kwarg (mirrors analytics_store pattern)

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* fix(presence): honor server stamp in status + stamp on codex error event

Addresses murzik's PR #280 review findings:

1. _agent_presence() status branch previously returned "unknown" for any
   agent without a heartbeat — even when last_seen_at was recently stamped
   by server-side activity. Status now falls through to server_ts when no
   fresher heartbeat exists, using the same online/idle/offline age bands.
   Heartbeat logic still wins when hb_ts >= server_ts so dead/non-alive
   heartbeats aren't masked by an older server stamp.

2. codex_session "error" event path now stamps last_seen. Previously only
   turn.completed and turn.failed stamped, so transport/runtime errors
   surfaced via the generic error event missed a real liveness signal.

Tests:
- tests/test_api.py — 5 new presence tests (server-stamped online/idle/
  offline, no-stamp-no-hb unknown, heartbeat-wins-when-fresher)
- tests/test_codex_session.py — 3 new stamp tests (error event stamps,
  plus regression guards for turn.completed and turn.failed)

1637 passed, ruff clean.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

---------

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
…#285) (#297)

search_by_embedding silently returned [] when the query embedding had
zero norm, making it impossible for callers to distinguish a broken
embedding from a legitimate 'no matches found' result.

- Add InvalidQueryEmbeddingError(ValueError) in pinky_memory.store.
- search_by_embedding_scored raises on empty or zero-norm queries.
- Inner _search_by_numpy keeps a defense-in-depth raise on zero-norm.
- MCP recall() in pinky_memory.server catches and logs the error before
  falling back to keyword search, so broken embeddings surface in logs
  instead of vanishing.
- Add regression tests covering zero-norm and empty query embeddings.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
)

* fix(outreach): let httpx set Discord multipart content type (#283)

Discord file uploads crashed because the adapter set
`Content-Type: application/json` as a Client-level default, then tried to
override it per-request with `headers={"Content-Type": None}`. httpx
rejects `None` as a header value and raises before sending, so
DiscordAdapter.send_file() never worked.

- Drop the Client-level Content-Type default; httpx now sets the correct
  content type per request (application/json for json= bodies,
  multipart/form-data with boundary for files= bodies).
- Remove the invalid `Content-Type: None` override in send_file().
- Add a regression test that exercises the real send_file() path via
  httpx.MockTransport and asserts a proper multipart request is emitted.

Co-Authored-By: Murzik <noreply@pinkybot.ai>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* style: ruff isort fix for test_discord.py

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

---------

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Murzik <noreply@pinkybot.ai>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
… (#299)

* fix(memory): validate and log embed() failures on reflect/recall (#291)

embed() failures (empty returns or raised exceptions) previously stored
empty embeddings silently. Downstream semantic search quietly missed
those memories with no signal that anything was wrong.

- Add _safe_embed() helper in pinky_memory.server that:
  - Skips the call entirely for NoOpEmbeddingClient (expected downgrade)
  - Catches exceptions from real embedders and logs with type + message
  - Logs a "degraded" warning when a non-NoOp embedder returns []
- reflect() now surfaces `embedded: bool` in its JSON response so callers
  can see when a memory was stored without a usable vector.
- recall() routes through the same helper — failures no longer masquerade
  as "no matches"; keyword fallback proceeds with a visible log entry.
- Add regression tests for: NoOp embedder, real embedder returning value,
  real embedder returning [] (degraded log), and embedder raising.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* fix(memory): treat zero-norm and non-finite embeddings as degraded (#291)

Murzik review: a real embedder returning a non-empty but zero-norm (e.g.,
[0.0, ..., 0.0]) or non-finite (NaN/inf) vector passed the previous
`if not embedding` check, so reflect() reported embedded=True and stored
an unusable vector. Downstream search then silently skipped those rows
via the existing zero-norm row guard in _search_by_numpy — reproducing
the original silent-degrade bug with different inputs.

- Extend _safe_embed() to validate non-empty vectors: zero-norm or any
  non-finite component → log "degenerate" and return [] so reflect()
  reports embedded=False and stores no vector.
- Add regression tests for [0.0]*8 and NaN-containing embeddings.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Murzik <noreply@pinkybot.ai>

---------

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
Co-authored-by: Murzik <noreply@pinkybot.ai>
… (#300)

sqlite-vec query exceptions were swallowed and _search_by_numpy was
invoked silently. Users got slower, possibly differently ordered search
with no signal that vec search was broken.

- Log a WARNING via pinky_memory.store logger with exception type and
  message before falling back to numpy.
- Track a per-instance `_vec_fallback_count` bumped on every fallback.
- Surface `vec_available` and `vec_fallback_count` in introspect() so
  health checks / dashboards can detect a degraded vec backend.
- Regression test simulates a vec failure via a connection wrapper and
  asserts the warning is logged, counter incremented, introspect reports
  it, and numpy fallback still returns results.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
WhatsAppAdapter.download_file() wrote the raw response body to disk
without checking the second-stage media GET status. 401/404 responses
(HTML or Graph API error JSON) were persisted as wa_<media_id>.<ext>
files, silently corrupting the media cache.

- Raise WhatsAppError on any status_code >= 400 from the media download.
- Extract Graph API error message/code when the response is JSON; fall
  back to the raw HTTP status otherwise.
- Existing happy-path test now asserts status_code=200 explicitly so the
  new guard is exercised.
- New regression asserts a 401 Expired-token response raises with the
  correct error_code/message and leaves no wa_<id>.* file behind.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Murzik <noreply@pinkybot.ai>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
Telegram media senders (send_photo, send_document, send_animation) now
filter None-valued fields from multipart form data. Passing
reply_to_message_id=None was being serialized as the literal string
"None" in the multipart body, which Telegram's API rejected or treated
as an invalid message id.

Patch authored by Murzik; applied + committed with attribution.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Murzik <noreply@anthropic.com>
KB self-tools now URL-encode search query params and path variables:
- `kb_search` builds its querystring via urllib.parse.urlencode so that
  queries containing `&`, `#`, `?`, or spaces round-trip correctly
  instead of silently injecting into the request URI.
- `kb_get_wiki` / `kb_save_wiki` / `kb_delete_wiki` quote the slug with
  `safe="/"` — nested slugs like `topics/llm-knowledge-bases` still
  work, but spaces / specials inside a segment are escaped.
- `kb_delete_raw` / `kb_update_raw` quote the raw source id as a single
  path segment (`safe=""`), so ids containing `/` or other specials
  no longer break routing.

Added regressions for query injection, wiki slug specials, and raw
source IDs containing `/` + space.

Patch authored by Murzik; applied + committed with attribution.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Murzik <noreply@anthropic.com>
`get_events()` now parses start_date / end_date inside its error
boundary and returns JSON `{error: ...}` for invalid ISO dates
instead of letting `ValueError` escape the MCP tool (which surfaces
as an unhandled exception to the caller).

Added a new `tests/test_calendar_server.py` with a regression for
invalid `start_date`.

Patch authored by Murzik; applied + committed with attribution.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Murzik <noreply@anthropic.com>
…es (#286) (#307)

`voice_routes.voice_websocket` was handing `_voice_store.get_session(...)`
directly to `finalize_call()` in the WS disconnect path. `get_session()`
can legitimately return None under session-expiry races (session was
cleaned up, or was never committed after an earlier failure), and
`finalize_call()`'s first line accesses `session.call_sid` — boom,
`AttributeError: 'NoneType' object has no attribute 'call_sid'`, and the
whole finalize path (Opus review + artifact + owner notification) breaks.

Two changes, belt + suspenders:

1. Call site (`voice_routes.py`): look up the session before scheduling
   `finalize_call`, skip + log if it's gone. This prevents the task from
   being scheduled in the first place.

2. Callee (`voice_engine.py::finalize_call`): accept None defensively and
   early-return with a log line. Any future caller that forgets the guard
   gets a clean no-op instead of a crash.

Added tests/test_voice_engine.py covering:
- `finalize_call(None, ...)` no-ops and does not touch voice_store / agents / broker
- `finalize_call(real_session, ...)` still proceeds past the guard

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
#294) (#308)

`datetime.utcnow()` is deprecated as of Python 3.12 (will emit
DeprecationWarning and eventually be removed). Naive `datetime.now()`
silently picks up local tz, which breaks any cross-tz serialization
or comparison and — in the case of streaming_session.py:300 — labelled
a local-time string as "UTC".

Five call sites swept:

- `pinky_daemon/api.py:2946` — version string now uses `now(timezone.utc)`
  so the built version is deterministic regardless of host tz.
- `pinky_daemon/api.py:6966` — web/DM timestamp fallback uses `now(utc)`.
- `pinky_daemon/api.py:7026` — broker-message timestamp fallback uses `now(utc)`.
- `pinky_daemon/streaming_session.py:300` — fallback labelled UTC now
  actually uses UTC (was implicit local-tz bug).
- `pinky_memory/kg_extractor.py:449` — hoisted out of the per-triple loop
  into a single cached `_now_ymd` value per `_resolve_functional_conflict`
  call. Already tz-aware; this is just the "cache once per call" cleanup
  flagged in the issue.

Verified: `grep -n 'datetime\\.utcnow\\|datetime\\.now()'` returns only
comments. Full test suite for touched areas (test_api, test_kg_extractor,
test_streaming_session) — 305 passed.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
#289) (#301)

_detect_embedding_dimensions() and _backfill_vec() both called json.loads()
on stored embedding blobs with no guard. A single corrupted row crashed
dimension detection (blocking startup) or logged nothing in backfill.

- _detect_embedding_dimensions() now scans up to 50 newest-first rows,
  skips malformed JSON with a logger.warning carrying the reflection id,
  and returns the first valid embedding's length.
- _backfill_vec() catches ValueError/TypeError on json.loads, logs the
  reflection id, counts corrupt rows, and reports them in the completion
  message. The bare `except Exception: pass` on the insert step is
  replaced with a targeted sqlite3.Error catch + debug log.
- Regression test corrupts one row's embedding, makes it the newest,
  asserts _detect_embedding_dimensions still returns 8 from the good
  row and that the bad row id appears in the warning log.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
…295) (#309)

Closes #295. Replaces 10 `except:` / `except Exception:` sites with
specific exception classes (mostly `sqlite3.OperationalError`) plus
logger calls at appropriate severity levels. Preserves fallback
semantics — callers still get numpy/LIKE fallbacks, migrations stay
idempotent. The one site calling user-supplied `llm_classify` keeps
a broad catch with a noqa BLE001 + justification since the callable
can raise anything.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Murzik <noreply@anthropic.com>
…287) (#306)

* fix(oauth): validate state nonce on legacy Google Calendar callback (#287)

**Security fix (high).** The legacy `/calendar/google/callback` endpoint
accepted `state` but never compared it against any stored value before
exchanging the authorization code and saving tokens. A crafted callback
hitting the owner's browser with an attacker-controlled code could bind
the wrong Google account.

Changes:
- New `/calendar/google/direct-auth-url` endpoint that calls the existing
  `pinky_calendar.oauth.get_auth_url()` *and* persists the returned state
  nonce with an ISO timestamp under `GOOGLE_OAUTH_STATE_{nonce}` (TTL 10m).
- `/calendar/google/callback` now requires + validates + single-use-consumes
  the state nonce before any client-credential lookup or code exchange.
  Missing / unknown / replayed / expired / corrupt / future-dated states
  are all hard-rejected with HTTP 400 + a readable HTML error page.
- State is deleted immediately on consumption (even on exchange failure
  or replay attempt), preventing TOCTOU reuse.
- `pinky_calendar.oauth.exchange_code`: updated the misleading
  "validated by caller" marker to a real SECURITY note that spells out
  the caller's obligation.

Tests (`TestGoogleOAuthStateValidation`): missing state, unknown/forged
state, expired state (purged afterward), replay rejection, state
consumption on exchange failure, direct-auth-url state persistence,
direct-auth-url credential gate.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

* fix(oauth): fail closed on state consume + reject naive timestamps (#287)

Addresses Murzik's review on PR #306:

**High — consume must fail closed.** `_consume_google_oauth_state()`
was ignoring both the return value of `delete_setting()` and any
exception it raised. That meant:
- If a concurrent consumer won the race (e.g. legitimate callback +
  attacker replay fired in parallel), *both* could proceed past the
  validation check.
- If deletion itself failed (DB locked, disk full, any OperationalError),
  the request still proceeded to token exchange.

DELETE is now the authoritative consume step:
- Returns False (row not deleted) → "unknown or replayed state"
- Raises → "could not consume state"
Either way, `exchange_code()` is never invoked.

**Medium — naive timestamps.** `datetime.fromisoformat()` accepts ISO
strings without a tz suffix and returns a naive datetime. Subtracting a
naive dt from an aware `now()` raises TypeError → the endpoint 500s
instead of returning the promised 400. We only ever *issue* aware UTC
timestamps, so naive records now hard-reject as "corrupt state record".

Regressions added:
- `test_consume_fails_closed_when_delete_returns_false` — simulates
  concurrent consumer, asserts callback 400 + `exchange_code` not called.
- `test_consume_fails_closed_when_delete_raises` — simulates DB failure,
  asserts callback 400 + `exchange_code` not called.
- `test_callback_rejects_naive_timestamp_state` — seeds a naive ISO
  string, asserts 400 + row purged.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>

---------

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
sync.sh/status.sh are no longer present in the repo. Replace with the
current deploy path: agents call update_and_restart() from pinky-self,
which honors PINKYBOT_CHANNEL to pick main vs beta. Also refresh the
Mac Mini LAN IP (.32 → .209) after the DHCP lease change on 2026-04-17.

Co-authored-by: Oleg <oleg@Olegs-Mac-mini.local>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
OpenAI shipped GPT-5.5 on 2026-04-23, available in Codex CLI via ChatGPT
sign-in auth today (API access coming "very soon"). Murzik was already
bumped to gpt-5.5 live via the API; this mirrors the change in the seed
list so fresh installs inherit it.

supports_thinking=0 to match existing OpenAI entries (reasoning effort
is driven by the agent-level thinking_effort field, which Codex maps to
model_reasoning_effort at spawn).

🤖 Opened by Barsik
Picks up:
- v0.1.68 (CLI v2.1.119)
- v0.1.67: trio compatibility fix (RuntimeError: no running event loop
  regression from v0.1.51 — uses sniffio-based dispatch). Adds explicit
  sniffio runtime dep.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@olegbrok
olegbrok merged commit ccdd8c2 into main Apr 25, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant