Changelog
All notable changes to pm-workspace-kit are documented here.
The format is loosely based on Keep a Changelog, and the project follows Semantic Versioning. Each release also has a longer narrative on GitHub Releases with rationale, dogfood notes, and test plans.
[v0.12.0] — 2026-05-08 — gateway: anthropic-api as default provider
Why
v0.11.1 hardened the gateway against msg_too_long by lowering caps and adding an auto-retry path, but cause #2 from the 2026-05-07 incident — claude-agent-sdk spawning the local claude CLI and inheriting the host's ~/.claude/ config (skills/hooks/MCP descriptions) as un-budgeted system context — was absorbed by tighter caps, not eliminated. v0.12.0 flips the default to the direct Anthropic SDK so SDK overhead is no longer a budget unknown, and restores cap headroom.
Spec: apps/docs/docs/plans/2026-05-08-gateway-anthropic-api-default.md. Migration: v0.12 migration notes.
Changed
- Default LLM provider auto-resolves to
anthropic-apifirst (wasclaude-agent). Soft flip — users withANTHROPIC_API_KEYset auto-switch; users without it stay onclaude-agentwith no behavioural change.PMK_PROVIDER=claude-agentstill pins the legacy path explicitly. - Cap defaults restored to operationally useful values now that SDK overhead is gone on the default path:
PMK_MAX_SESSION_TOKENS25_000 → 60_000PMK_SEED_CAP12_000 → 30_000PMK_MRA_RESULT_CAP16_000 → 40_000
gateway initprompts forANTHROPIC_API_KEYafter Slack tokens; stored in~/.pmk/gateway.jsonapiKeyfield at mode 0600. Empty input keeps existing value or falls back to env var. The running gateway daemon needs a graceful restart to pick up a newly-set apiKey (matches the existing audience/escalation config-mutation pattern).
Added
token.usageevent inevents-YYYY-MM.log— emitted byAnthropicApiKeyProvider.chat()after each successful stream completion, when anactoris provided inChatOptions. Fields:actor,provider,model,inputTokens,outputTokens, optionalcacheReadTokens/cacheCreationTokens. Best-effort write — failures don't break the chat.Token usagesection inpmk gateway auditrolls up the new events: total in/out, cache read (when non-zero), top-3 per-actor by input tokens, per-model breakdown.ChatOptions.actoroptional field on theLlmProvider.chat()interface for usage attribution. Threaded throughchatWithContextRetryautomatically; CLI command-side wiring is future work.
Tests
@pmk/cli 304 → 312 (+8): resolver.ts autoResolve order (apiKey-preferred + fail path), AnthropicApiKeyProvider.chat() token-usage emission with mocked stream + finalMessage(), no-emission when actor undefined, events.ts round-trip for token.usage, audit.ts aggregation, audit-format.ts Token usage rendering for non-zero + zero cases. Cap-default test assertions flipped from v0.11.1 values to v0.12.0 values.
Forward-looking
claude-agent provider stays as a soft-flip fallback indefinitely. Re-evaluate deprecation in v0.13+ based on usage data from the new Token usage audit section. $-cost calculation is a v0.13+ candidate, gated on a stable price-table source. SlackGateway integration harness remains tracked as a v0.11.2 follow-up.
[v0.11.1] — 2026-05-07 — gateway msg_too_long hardening
Why
A live Slack thread on 2026-05-07 returned pmk 內部錯誤:An API error occurred: msg_too_long after several mra-ask rounds. Root-cause analysis surfaced four issues and v0.11.1 layers defenses against all of them so the failure mode does not reach production users again. See apps/docs/docs/plans/2026-05-07-gateway-msg-too-long-hardening.md for the full design spec, and 2026-05-07-gateway-msg-too-long-hardening-implementation.md for the per-task TDD plan.
Fixed
msg_too_longno longer reaches end users. Three layered defenses:- (a)
pruneSessionIfNeedednow runs before the LLM call (was after — closed a fail-loop introduced in v0.8.1 where a session over budget could never recover because prune only fired after a successful call). - (b) The PKB seed and
mra-askresults are capped at write-time so a single bloated message cannot single-handedly exhaust the input window. - (c) Any residual
msg_too_longtriggers a typedPmkContextTooLongError, an automaticforcePruneToMinimum, and a retry. The reply is prefixed with:scissors: 對話過長,已自動裁掉 N 輪舊訊息so users know context was trimmed. Hard failure (both calls reject) shows:x: 對話太長,請開新 thread 重新提問instead of the raw API error.
- (a)
Changed
PMK_MAX_SESSION_TOKENSdefault lowered 60_000 → 25_000 to leave headroom for system prompt, retrieval prefix, the SDK-inherited host context (claude-agent-sdkspawns the localclaudeCLI, which inherits~/.claude/skills/hooks/MCP descriptions), the new turn, and the model's reply.
Added
- New env vars
PMK_SEED_CAP(default 12_000 chars) andPMK_MRA_RESULT_CAP(default 16_000 chars) for per-host tuning. The previously-hardcoded 24_000-charmra-asktruncation inbuildMraSuccessMessageis replaced byPMK_MRA_RESULT_CAP. - New event types in
events-YYYY-MM.log:context.exceeded(withphase: "first-call" | "synthesise"),context.force-pruned,message.capped(withkind: "seed" | "mra-result"). pmk gateway auditgains aContext safetysection rolling up the new events. Tighten the*_CAPenv vars ifcontext.exceededappears in your weekly audit.- Helper
chatWithContextRetryextracted topackages/cli/src/gateway/slack/context-retry.tsso the retry+force-prune+events pattern is unit-testable in isolation (noSlackGatewayintegration harness needed) and reused at both LLM call sites (runFreeChatTurnfirst-call,synthesiseAfterMramra-ask round).
Tests
@pmk/cli 274 → 304 (+30): unit coverage for capMessageContent, forcePruneToMinimum, pruneSessionIfNeeded extras-aware budgeting, approxTokensFor with extra param, PmkContextTooLongError detection, the six-discriminant chatWithContextRetry (happy / non-context error / context-then-success-with-scissors / context-then-fail / dropped=0 degenerate / phase=synthesise audit), audit contextSafety rollup, formatter Context safety section non-zero + zero-count rendering, and the three new event-type round-trip in gateway-events.test.ts.
The seed-cap and mra-result-cap wiring sites in slack/index.ts and the runFreeChatTurn retry-prefix wiring rely on the constituent helpers' unit tests + manual verification (no SlackGateway integration harness in this release; tracked as a follow-up).
Forward-looking
v0.12 is planned to switch the gateway provider from claude-agent-sdk to anthropic-api, removing the SDK-inherited host-context as a budget unknown. The cap mechanism from v0.11.1 stays; only the budgets relax toward the model's true context window. See the v0.12 stub at the end of the v0.11.1 design spec.
[v0.11.0] — 2026-05-05 — gateway presence + per-channel audience + monthly audit logs
GitHub release · closes #23, #44 · milestone v0.11
Why
Two issue-driven items plus one v0.10.x debt cleanup, sized to ship as one minor release:
- #44 — kill→restart cycles broadcast spurious "重新上線" (live-observed during v0.10.0 verification).
- #23 —
pickAudiencehad no channel tier, forcing per-user overrides for "this channel defaults to exec" cases. - events.log unbounded growth TODO from v0.10 — bumped in priority because #44 adds presence events on every start/stop.
See the v0.11 migration notes for a focused operator-facing summary of the layout + behaviour changes.
Added
- Per-channel audience override (#23) — new
cfg.audience.channels: Record<channelId, AudienceKey>tier between per-user and workspace default. CLI:pmk gateway audience set-channel <channelId> <key>/unset-channel. Slack admin:/pmk admin audience set-channel #channel <key>/unset-channel.extractChannelIdhelper handles<#C0X|name>mention,<#C0X>bare mention, and rawC0X/G0X/D0XIDs. Resolution order at turn time: per-user → per-channel → workspace default. - Graceful-shutdown marker (#44) — single-use file at
~/.pmk/gateway/shutdown-markerwritten onSIGTERM/SIGINT. The nextstartHeartbeat()reads + consumes it to distinguish "kill -> restart" from a real crash;wasOffline=falseand the back-online broadcast is suppressed when the offline gap is under 5 minutes. - Presence event types in
events.log(#44) —gateway.onlineandgateway.offlinejoin the JSONL stream with monotonic per-processseq, human-readablereason(crash-recovery/graceful-fast-restart/graceful-long-downtime/shutdown),broadcastbool, andofflineDurationMs. Lets the audit detect rapid restart cycles and the graceful-vs-crash split. - Monthly-partitioned JSONL ledger (PR #47) — new
packages/cli/src/gateway/monthly-jsonl.tsshared util powers bothevents.logandadmin.log. Files are now~/.pmk/gateway/events-YYYY-MM.log/admin-YYYY-MM.log(UTC month). Legacy single-file ledgers from v0.10 are still read-only-merged so upgrades don't lose history. No eviction — operators canrmancient partitions manually; the reader silently skips missing months.
Fixed
- Restart-cycle broadcast spam (#44) — heartbeat is no longer deleted on graceful shutdown (it stays for
offlineDurationMsaccounting), andbroadcastBackOnline()checks the gap before posting. The same change exposes the issue's secondary symptom:broadcast()'s O(N) serial fan-out is nowrunWithConcurrency(limit=3), finishing in seconds instead of 20+ s and isolating per-recipient errors. Live-Slack verified: a 1.3-second graceful restart recordsgateway.online ... broadcast:false offlineDurationMs:1332and the channel sees no spurious "重新上線" message. events.logunbounded growth (v0.10.x debt) — closed by the monthly partitioning above.
Tests
248 → 274 (+26 across @pmk/cli). Major additions:
- Heartbeat marker decision matrix (5 branches: first boot, marker fresh, marker stale, no-marker fresh heartbeat, no-marker stale heartbeat) + corrupt-marker safety + upgrade-migration story
runWithConcurrency(4 cases: empty list, peak in-flight respected, single-task rejection isolated, limit > task count)pickAudiencechannel tier (5 cases: channel applies absent user, per-user beats channel, fall-through, undefined channelId, back-fill on old config) + empty-string channelId guard/pmk admin audience set-channel/unset-channelend-to-end (mention wrapping, raw ID, garbage rejection, round-trip)- Monthly partitioning (current-month write + legacy NOT written, legacy + partition merge order, multi-month aggregation with
sinceMscutoff, default 12-month window, legacy mixed-content malformed-line skip, admin-log mirror)
Plus a new @pmk/shared test surface (was 0 → 24): shape-based snapshot tests covering BASE_RULES, all four audience prompts, pickGatewayPrompt round-trip, AUDIENCE_KEYS, PROMPTS map coverage, and DEFAULT_CONFIG shape.
Total across the workspace: 274 → 324 pass, 0 fail.
Operator note
Zero migration. All schema changes are additive and back-fill-compatible. First kill→restart after upgrade still broadcasts "重新上線" once — the v0.10 gateway shut down without writing a marker, so the v0.11 build correctly treats it as a fresh boot. From the second graceful restart onward, suppression works.
For tail-style debugging, switch from tail -f ~/.pmk/gateway/events.log to tail -f ~/.pmk/gateway/events-$(date -u +%Y-%m).log (note the -u for UTC, since partitions roll on UTC month boundaries).