| 24 May 20269:30 pm |
koda-core |
2.120.4current |
fix(work): completion classifier + halted state file — two bugs surfaced by today's task #334 auto-run. (1) The 'all tasks complete' false-positive: /work auto and /work next/start treated 'no pending tasks' as 'all done', emitting a green '✅ All tasks complete across all queues' message even when the queue's only task was status=partial or failed. Now distinguishes the three cases: pending>0 → 'prereqs not met', partial+failed>0 → '⚠️ Queue halted — N partial/failed task(s) need attention', else → 'all complete'. (2) The auto-resume phantom: when the work-loop hits partial-detect or task-failed and breaks, the state file is deleted at loop exit — but if the parent Core has been killed mid-flight by a restart triggered earlier in the same step (the koda-agent-retire pattern), the cleanup never runs and the verify script auto-resumes a phantom 'running' queue. Now the work-loop writes state.status='halted' + state.haltedReason='partial'|'failed'|'stopped' BEFORE the break, persisted to disk before any await — the verify script's `state.status === 'running' || === 'interrupted'` check at scripts/self-modify.sh:594 falls through and skips auto-resume. Follow-up tasks filed for deeper fixes: #355 (subagent prompt should preflight-enforce declared file completion), #356 (verify script should run the acceptance check too — survives restart), #357 (architecturally consider per-step queue rows so partial states are structurally impossible). lib/commands/work.js is hot-reloaded; no Core restart needed. |
| 24 May 202611:40 am |
koda-core |
2.120.3 |
fix(security): ownership checks on /api/tasks/bulk-action + /api/tasks/work — task #322. The two batch endpoints previously accepted any task ID in the request body and operated on them without verifying the caller created the task or had admin+ role. Any authenticated non-Owner user could mark-done / delete / generate-instruction-files for ANY task by ID. GET filtering from #301 didn't help — IDs travel in POST bodies, not query params. Patch adds a private `assertTaskOwnership(user, ids)` helper in lib/api/tasks.js that fetches each task by id, checks `created_by === user.uuid || hasRole(user, 'admin')`, and returns an atomic refusal (403 if any task fails the check, 404 if any ID is missing — partial success rejected as confusing/un-auditable). Helper wired into bulk-action handler, /work handler, and (refactor) the existing single-task PATCH/DELETE handlers — they now call assertTaskOwnership([id]) instead of duplicating the inline check. TDD: 6/6 cases pass in tests/api-endpoints/tasks-ownership.test.js (owner success, viewer-own success, viewer-mixed 403 atomic, viewer-delete 404, viewer-work 403 atomic, admin bypass success). tests/api-endpoints/tasks-permission.test.js still 5/5 (no #301 regression). Work originally landed in 7a8ad567b (RED test) + f998f3e77 (GREEN fix) during a prior /work auto session; this bump closes the loop with the version + changelog entry that the auto-run dropped due to a plan-file status bug (separately fixed in 2.120.2's sanity gate). |
| 24 May 202611:30 am |
koda-core |
2.120.2 |
fix(work): auto-resume sanity gate prevents phantom-run after cross-session completion. Symptom (observed 2026-05-23): /work auto fired step 1 of task-322, committed the RED test, restarted Core for step 2's GREEN edit, the verify script auto-resumed via /internal/work-continue → /work auto task-322-bulk-work-ownership. By the time auto-resume fired, the instruction file had been archived (step 5 close-the-loop), so findAllUnits() returned nothing matching the filter and dispatch failed with 'No queue or standalone matching ...' — a dead-end that left the owner unsure if the work landed. Root cause was actually compound: (a) the prior session's agent skipped the plan-file in_progress flip on step 1, leaving it pending after a successful commit — verify script only transitions in_progress→done, never pending→done, so the phantom pending persisted; (b) /work auto had no sanity gate on filter resolution failure. This patch addresses (b): when /work auto X finds 0 matches in instructions/, it now scans instructions/archive/ for the same name; if found, reports '✅ Work for X already completed — found in archive as Y' and clears the tmp/koda-work-auto-{groupId}.json state file so subsequent restarts don't keep retrying the phantom. (a) is a procedural issue documented in CLAUDE.md but not enforced in code — out of scope for this patch. lib/commands/work.js is hot-reloaded; no Core restart needed. |
| 23 May 20263:45 pm |
koda-core |
2.120.1 |
fix(work): /work next prereq resolver now handles cross-task references. Previously the lookup only scanned `queue.tasks` for a matching `num`, so any `Depends on: Task #N` reference where the referenced task lived in a different queue (or was already archived) was treated as unmet — blocking standalone instruction files like task-322 which depends on task-301 (done in 2.120.0). The patch adds two fallback lookups in lib/commands/work.js findNextTask: (1) cross-queue scan — match a `#N` against any other queue/standalone whose task.file matches `^task-N(-|\.)`; (2) archive lookup — check `instructions/archive/` for a `task-N-*.md` file, treat as done if found. Also archived the stale task-301-non-owner-dm-tasks-filter.md (work landed in 2.120.0 — commits 88bb331d1, d2021f76f, 7fe088a9c, 8ed6b7bf1, b7b0490a7, b379e8dfd; step 5 koda_agent cleanup deferred to task #334 intentionally). Together these two fix the symptom (`/work next task-322` returned 'prerequisites not met' even though #301 was complete) and the underlying gap (cross-task prereqs were structurally unresolvable). lib/commands/work.js is hot-reloaded — no Koda Core restart needed. |
| 23 May 20262:00 pm |
koda-core |
2.120.0 |
feat(security): non-Owner /tasks (slash + HTTP) now filters to scope=global OR scope=app/group where the caller has a workspace_user_roles row linking the workspace to that surface — closing task #301's latent permission gap. Owner short-circuits and sees everything (unchanged). Two new helpers in lib/users.js: getAppsForUser(uuid) joins workspace_user_roles → workspace_surfaces filtered to surface_type='app'; getGroupsForUser(uuid) does the same for surface_type IN ('signal_group','whatsapp_group'). Both paths (lib/api/tasks.js GET handler + lib/commands/tasks.js /tasks branch) call them identically: explicit scope=app|group with a non-member scope_id returns empty (200/'No tasks found', NOT 403); no-scope DM builds a PostgREST `or=` clause covering global + member-app + member-group. RED-first TDD: tests/api-endpoints/tasks-permission.test.js seeds a viewer fixture with workspace+surface+role rows and asserts 5 cases (owner sees all, viewer filtered no-params, viewer 403-free filter on non-member app+group, viewer scoped on member app). Bonus cleanup deferred: drift check found the koda_agent role is much more provisioned than initial investigation showed (18 RLS policies + 62 grants spanning ~38 tables, not just one stub policy on tasks); rather than half-doing it here, filed task #334 (instruction file + queue at instructions/task-334-retire-koda-agent.md and instructions/koda-agent-retire-queue.md) for a clean-context retirement of the whole apparatus. Also filed #322 (P3, no ownership checks on /api/tasks/bulk-action + /work) and #323 (P4, extract buildTaskScopeFilter() so slash + HTTP stop reimplementing scope filtering). This is a clean precursor to v3.0's 4-role RBAC + multi-channel work. |
| 23 May 20269:17 am |
koda-core |
2.119.2 |
fix(btw): /btw side-channel timeout default raised from 2 min → 5 min so forked Claude sessions actually have room to finish (task #319). Logged user-visible failures on 2026-05-01, 2026-05-08, and 2026-05-21 all hit the same 120s wall — and the user's own /btw 'how do I change this timeout?' query timed out, which is how the bug self-documented. Two-site change: lib/claude.js runBtw() default timeoutMs 120_000 → 300_000; lib/commands/btw.js call-site explicit override 120_000 → 300_000 (kept explicit at the call site so readers can see what's being asked for). Also fixed the lying error string in runBtw — previously hardcoded 'Timed out after 2 minutes' even when the caller passed a different timeoutMs, now templated as `Timed out after ${Math.round(timeoutMs/60_000)} minutes` so the surfaced message matches reality. Hot-reload not available for lib/claude.js, so step 1 forced a Core restart; step 2 (lib/commands/btw.js) was hot-loaded with no restart. |
| 21 May 20265:30 pm |
koda-core |
2.119.1 |
fix(queue): two related bugs surfaced after the 2026-05-21 gmail/drive plan. (1) Stuck-claim deadlock — processNext pre-claimed the group before re-dispatching a queued envelope, but its release lived only inside the Claude invocation's finally; any early-return path (slash command without _planMessage, cancel phrase, permission denial, dedup-suppressed message, throw in pre-Claude prep) leaked the claim and stuck the group busy forever. Symptom: the Signal DM (cab1a1ff-...) went silent — every new message hit isGroupBusy() === true, was queued, and silently expired after 10 minutes. (2) Auto-continue duplicate — the verify script's /internal/plan-continue HTTP fires a synthetic /continue envelope (_autoContinue: true) after every restart, but the previous step's queued /continue was also persisted to disk by saveQueues during SIGTERM and restored on startup; both were then dispatched, doubling up every cycle (saw 'queued (#2)' on every auto-mode plan). Fix: lib/message-queue.js processingGroups refactored from Set to per-groupId counter so claims can be balanced across nested processNext recursion without ownership ambiguity; processNext gains .finally(releaseGroup) to always release its pre-claim regardless of processMessage outcome; saveQueues filters out envelope._autoContinue entries so the verify script's HTTP call doesn't race a restored stale copy of itself. server.js gets a comment at the line-692 claim site documenting the counter ownership rule. Step 2's restart cleared the live stuck claim on cab1a1ff-... |
| 21 May 20269:35 am |
koda-core |
2.119.0 |
feat(gmail+drive): read-only Gmail and Drive access via per-user OAuth. New chokepoints lib/gmail.js (searchMessages/getMessage/getThread/listLabels) and lib/drive.js (searchFiles/getFileMetadata/getFileContent/listChildren) sit alongside the existing lib/calendar.js (service-account model). New shared helper lib/google-oauth.js owns google-auth-library and constructs OAuth2 clients with refresh-token write-back via lib/cred.setCred — Google-side rotations are persisted automatically so the next call never fails with stale-token invalid_grant. lib/cred.js gains setCred() (upsert on credentials.domain, audit-logged as cred.set). scripts/google-oauth-init.js runs the one-time interactive auth flow over a localhost:53682 loopback callback; persists refresh_token + access_token + expiry + scopes as a JSON value in cred google-user-token / username=<userId>. eslint.config.js extended to multi-owner — googleapis is now co-owned by calendar/gmail/drive. Two new bundles: bundles/gmail.yaml + bundles/drive.yaml (bundle-as-docs, same pattern as calendar). docs/chokepoints.json gains three new entries (google-oauth, gmail, drive). CLAUDE.md capability table mentions drive_read. docs/information-flow.md gains a Google Integrations section comparing the SA + OAuth models. Known follow-ups: drive_read not yet in GRANTABLE_CAPABILITIES nor the in-prompt Capability-Gated Features block (owner bypass covers current use); credentials table has UNIQUE on domain alone so multi-user OAuth tokens would need a composite unique. |
| 18 May 202611:13 am |
koda-core |
2.118.0 |
refactor(attachments): Phase 5 rename attachments_v2 → attachments (task #289 complete). Atomic DDL renamed table + sequence + PK constraint + 2 CHECK constraints + FK + 7 indexes + unique (source, file_path) index + 2 RLS policies in a single transaction; created backwards-compat VIEW attachments_v2 WITH (security_invoker=true) so each lib/*.js update was independently restart-safe, then dropped after all code references migrated. Code updates: lib/attachments-unified.js (insertUnifiedBestEffort), lib/api/task-attachments.js (pgGet/pgDelete + V2_TASK_SELECT → TASK_ATTACHMENT_SELECT), lib/context-backlog.js (attachment placeholder lookup), lib/crypto.js (sha256HexFile comment). schema.sql canonical refresh — all 14 DDL refs renamed. Test suites refitted: tests/api-endpoints/attachments-api.test.js (7 refs + dropped dead Phase-4 task_attachments asserts) and tests/lib-modules/attachments-unified.test.js (16 refs incl. CHECK constraint name patterns; 5/5 pass live). No data movement — pure identifier rename. Polymorphic table now under its final name; the _v2 suffix is gone from the codebase. |
| 18 May 20269:50 am |
koda-core |
2.117.0 |
refactor(attachments): Phase 4 cutover — attachments_v2 is now the sole source of truth (task #289). Dropped dual-write from lib/attachments.js (saveAttachment, saveTaskAttachment) and lib/api/task-attachments.js (POST + DELETE handlers); removed unused pgPost import. Backed up legacy tables (pg_dump → tmp/archive/legacy_attachments_backup_20260518.sql, 458 + 22 rows) then DROP TABLE attachments + task_attachments in a single transaction; NOTIFY pgrst schema reload. attachments_v2 unchanged at 480 rows (459 legacy mirror + 21 task mirror + 1 smoke-test row from task 284). schema.sql cleaned of legacy DDL (CREATE TABLE, indexes, grants, sequence grants, ALTER TABLE … ENABLE RLS, agent_attachments + koda_core_attachments policies). Archived scripts/backfill-attachments-v2.js (legacy tables gone). Removed tests/lib-modules/attachments-backfill.test.js; refitted tests/lib-modules/attachments-unified.test.js to keep schema-contract + RLS describes (5/5 pass live). NOT renaming attachments_v2 → attachments — purely cosmetic, deferred to a possible Phase 5. |
| 18 May 20269:17 am |
koda-core |
2.116.0 |
refactor(attachments): unified attachments_v2 table for chat + task attachments (task #289 Phases 1-3). Polymorphic table with `source` discriminator (signal | whatsapp | task) replaces the split between legacy `attachments` (chat) and `task_attachments` (PWA tasks). Phase 1: schema + RLS policies + dual-write helper (lib/attachments-unified.js) + sha256HexFile chokepoint in lib/crypto.js. saveAttachment / saveTaskAttachment now best-effort dual-write to v2 with legacy as source of truth. Phase 2: one-shot idempotent backfill script (scripts/backfill-attachments-v2.js) — source inferred from group_id suffix (@g.us / @s.whatsapp.net → whatsapp). UNIQUE (source, file_path) index makes re-runs no-ops. Computes content_hash opportunistically when file exists on disk. Prod run scanned 486, inserted 3, duplicates 483, hashed 477, errors 0. Phase 3: read paths switched — lib/context-backlog.js reads attachment placeholders from attachments_v2 using PostgREST alias select (file_type:mime_type) to preserve legacy response shape; lib/api/task-attachments.js list/get/delete/upload all use attachments_v2 with source='task' filter. Returned rows use PostgREST alias select (file_size:size_bytes, uploaded_by:uploader_uuid) so frontend stays unchanged. Delete cleans up BOTH tables via (task_id, file_path) join key. Integration tests against running Koda Core + real PostgREST (tests/api-endpoints/attachments-api.test.js, tests/lib-modules/attachments-backfill.test.js, tests/lib-modules/attachments-unified.test.js) cover upload, list, fetch, delete, dual-write parity, backfill correctness, idempotency, and source classification. Phase 4 cutover (drop dual-write, rename, drop legacy tables) deferred ≥2 weeks as new task. |
| 17 May 20268:20 pm |
koda-core |
2.115.3 |
chore(observability): demote 'Claude prefix fingerprint' log line from INFO to DEBUG in lib/claude.js (task #312 follow-up). Investigation concluded — root cause of turn-2 cache miss anomaly identified as upstream Claude CLI cache_control marker placement, not Koda. Diagnostic kept in code at DEBUG level in case Anthropic asks for repro data; tag signals it's non-operational to future log readers. Note: lib/logger.js currently writes all levels to the same file, so volume is unchanged — DEBUG tag is for downstream filtering and intent signalling. |
| 17 May 20267:25 pm |
koda-core |
2.115.2 |
feat(observability): add per-turn 'Claude prefix fingerprint' INFO log line in lib/claude.js immediately before --print payload submission (task #312). Logs systemPromptLen + sha256Short(systemPrompt), preambleLen + sha256Short(dynamicPreamble), and userPayloadLen, alongside groupId and sessionId. Purpose: root-cause the turn-2 cache anomaly observed in task #286 validation (cacheRead=16,195 vs cacheCreate=78,376 instead of expected near-zero creation) by capturing whether the stable system prompt or the dynamic preamble drifts between consecutive turns. Hash diffing across log lines will reveal whether prefix instability is causing the cache to re-create rather than read. Required a new sha256Short(value, len=12) helper added to lib/crypto.js — the chokepoint owner of node:crypto under the EIP per-owner exemption — to keep ESLint's no-restricted-syntax rule satisfied without resorting to an eslint-disable escape hatch. Diagnostic is permanent for now; will be converted to debug-level or removed once root cause is identified in a follow-up task. |
| 17 May 20267:00 pm |
koda-core |
2.115.1 |
feat(observability): add permanent Claude cache-stats INFO log line in lib/claude.js stream-json result-event handler (task #286). Captures usage.cache_read_input_tokens, cache_creation_input_tokens, input_tokens, output_tokens per invocation, alongside groupId and sessionId. Validates the May 13 --append-system-prompt + --exclude-dynamic-system-prompt-sections refactor in real-world Signal traffic — grep `Claude cache stats` in logs/bridge.log to verify turn 1 shows creation>0/read=0 and turns 2+ within 5min show read>0 (cache hit). Option 1 SSH probe already confirmed cache works in synthetic --resume sessions (turn 2 read ~27K, cache_create only ~18 fresh tokens). This log line closes the observability gap so future regressions get caught early. |
| 17 May 20263:08 pm |
koda-core |
2.115.0 |
refactor(rbac): rename app_user_roles table → workspace_user_roles + fix assign_role intent param app → workspace_id (task #295). The table was historically misnamed — it's keyed on (user_uuid, workspace_id), one row per user per workspace, not per app. Full rename across DB (ALTER TABLE + sequence + constraints + indexes + RLS policies), schema.sql, all 9 code call sites (lib/users.js, lib/api/{workspaces,users}.js, lib/sync.js, lib/commands/{user,whocan,workspace}.js, server.js), and docs (CLAUDE.md, docs/identity-action-security.md, docs/koda_spec_v3_0.md). Zero-downtime via a backward-compat VIEW alias that was dropped only after all code paths migrated. The assign_role intent now takes 'workspace_id' instead of 'app' — usermgmt manifest updated and re-registered, worker handler accepts both 'workspace_id' (new) and 'app_name' (legacy alias) to preserve compat with the unchanged D1 column name in the usermgmt worker (rename of that column is OUT of scope — separate future task). Worker redeployed via wrangler chokepoint. |
| 17 May 20261:40 pm |
koda-core |
2.114.2 |
fix(whatsapp): burst-batching window collects straggling media envelopes before invoking Claude. Root cause: WAHA delivers each WhatsApp media attachment as a SEPARATE webhook envelope. When the owner sends N items as a burst with the caption on the first one, the first envelope wins the per-group claim lock in lib/message-queue.js and reaches the Claude invocation in processMessage() before envelopes 2..N arrive — those later envelopes get queued and eventually drained, but the first call to Claude only sees attachment #1. Symptom: 3 pics sent to a WhatsApp group, only the first was acknowledged. Fix is two-part: (1) lib/message-queue.js gains peekQueue(groupId, predicate) and dropFromQueue(groupId, predicate) helpers — peek returns matching entries without dequeuing, drop removes and returns them; both filter by predicate so server.js can target media-only entries from a specific sender LID. (2) server.js processMessage() inserts a 1500ms burst-batching window after the immediate ack send and before prompt-build, gated on channel === 'whatsapp' and !envelope._fromQueue: drains media-only entries (no text, attachments.length > 0) from the same sender LID via dropFromQueue, saves their attachments via saveAttachment(), merges them into savedAttachments[], and calls messageQueue.clearSeen() on the dedup keys so the entries don't replay. Downstream prompt builder already handled multi-attachment arrays — the data just never reached it before. Covers both pics and videos (e.g. .mp4 stragglers in the same burst). |
| 17 May 202610:45 am |
koda-core |
2.114.0 |
feat(reminders): user-facing reminders — discrete from scheduled_tasks. New `reminders` PostgREST table + 60s poller in `lib/reminders.js` (mounted in server.js after scheduler.init); fires via the channel router so Signal/WhatsApp share one code path. Two surfaces: (1) natural language via the new `koda.remind` intent — Claude parses 'remind me/us/@X to ... [time]' into a koda-intent block, dispatched in `lib/intents.js` to `createReminder()`; system-prompt block injected by `lib/instructions.js` teaches the format incl. Melbourne timezone parsing and @-mention recipient resolution. (2) `/remind` slash command (`lib/commands/remind.js`) for manual list/add/snooze/cancel/done with creator-or-owner ownership checks. Supports daily/weekly/monthly recurrence, DM and group delivery modes. `bundles/reminders.yaml` declares the chokepoint actions + permissions (reminders.read/write) with cancel_reminder flagged destructive. Schema: id, message, fire_at, recurrence, recipients[], group_id, channel, delivery_mode, status, snoozed_until, fired_at, created_by — indexed for the poll path (partial on fire_at WHERE pending) and per-user GIN on recipients. |
| 17 May 20269:51 am |
koda-core |
2.114.1 |
fix(reminders): default recipients=[actorUuid] for DM mode when params.recipients is empty. The natural-language 'remind me' flow failed validation because Claude doesn't have the sender's UUID in the prompt — emitted intents with recipients=[], which createReminder() then rejected. The koda.remind handler in lib/intents.js now resolves delivery_mode and group_id first, and when recipients are empty AND the call is DM (delivery_mode='dm' and no group_id) the actor UUID is used as the sole recipient. Group reminders still require explicit recipient or group context. Filters falsy entries out of explicit recipient arrays as a side benefit. |
| 16 May 202610:45 pm |
koda-core |
2.113.0 |
feat(intents): consolidate grant_capability / revoke_capability batch replies into a single per-target summary. Previously a batch of N grants emitted N 'Granted X' lines plus a generic '✓ Done — N actions' footer (4 messages for 3 grants). Now: the internal handler (_executeInternalCapabilityIntent in lib/intents.js) returns structured results instead of calling sendFn directly; a new _renderCapabilitySummary helper groups by (kind, target, workspace) and renders one block per target ('✓ @Klaus (workspace abc1…)\nCapabilities (3): research, transcribe, calendar_read'); executeBatch collects capability results across the loop and emits the consolidated summary at the end, suppressing the generic footer for capability-only batches. Single grants keep the historic terse one-liner. Mixed batches (capability + non-capability) still get both the consolidated cap summary and the generic footer for the non-cap items. |
| 16 May 20265:20 pm |
koda-core |
2.112.1 |
fix(audit): logAudit shim now forwards channel parameter to logAction — unblocks WhatsApp LID grants. The 6-arg shim at lib/logging.js:120 silently dropped channel info, falling through to logAction's default 'web'. WhatsApp-originated grants were tagged channel='web', so the channel-gated LID regex at server.js:1035 never ran, the LID never entered verifiedIdentities, and Gate A in lib/intents.js:314 rejected every grant. Step 1: lib/logging.js logAudit signature extended with optional channel arg (back-compat default null, spread-guarded). Step 2: 7 server.js call sites (L241, 430, 612, 618, 1060, 1151, 1155) now pass channel from handler scope. |
| 16 May 20264:55 pm |
koda-core |
2.112.0 |
feat(chunking): symmetry pass between Signal + WhatsApp chunk limits. MAX_SIGNAL_CHUNK default bumped 1500→65536 to match MAX_WHATSAPP_CHUNK; the previously-hardcoded WhatsApp limit is now env-driven via new config.MAX_WHATSAPP_CHUNK. Both vars are exposed through GET /api/config (SAFE_CONFIG_KEYS) and updatable via PATCH /api/config + /config command (CONFIGURABLE_KEYS + UPDATABLE_KEYS). Effect: chunking effectively disabled for any realistic message length on both channels; user requested to observe behaviour without the chunker for a while. Files: .env, .env.example, lib/config.js, lib/channels/whatsapp.js, lib/api/config.js, lib/commands/config.js. |
| 16 May 202612:20 pm |
koda-core |
2.111.6 |
fix(plan-continue): WhatsApp auto-continue now actually fires — buildPlanContinueEnvelope WhatsApp branches were missing _resolvedWhatsappId/_senderLid/sourceNumber so user resolution failed and groupId resolved to OWNER_UUID (Signal) instead of the WhatsApp chatId. Fix adds WAHA lid-mapping reverse-lookup for @lid chatIds and @c.us suffix-strip path so the synthetic envelope now mirrors the real inbound shape (server.js:1801-1813). Group (@g.us) branch carries groupInfo + _resolvedChatId. Signal/PWA/UUID branches unchanged (regression guard). TDD: tests/lib-modules/plan-utils-whatsapp-envelope.test.js (18 tests). Closes Task #311. |
| 16 May 202611:58 am |
koda-core |
2.111.5 |
fix(notifications): plan-aware restart notifications now stay on the originating channel (corrects 2.111.4 broadcast-everywhere). Core lifecycle messages (startup 'online', shutdown 'shutting down', version-bump 'Updated: …' line) dispatch to a configurable control-plane channel (system_config.control_plane_channel, default 'signal', 60s cache) plus any in-flight plan's channel via new channels.sendLifecycle(). server.js 3 lifecycle call-sites switched from channels.broadcastToOwner() to channels.sendLifecycle(); /internal/broadcast-owner endpoint left as-is for non-lifecycle bash callers. scripts/self-modify.sh notify_owner() and inline VERIFY_SCRIPT notify() both now read .channel from the active plan file (jq '.channel // "signal"') and pass --channel <X> to notify.sh instead of hardcoded --channel both. Future PWA-as-primary is a no-code DB write: UPDATE system_config SET value='whatsapp' WHERE key='control_plane_channel'. TDD: tests in tests/lib-modules/lib-channels-sendLifecycle.test.js (13), tests/api-endpoints/server-lifecycle-dispatch.test.js (5), tests/scripts/self-modify-channel-from-plan.test.js (6). |
| 16 May 202610:30 am |
koda-core |
2.111.3 |
Restart notifications now mirror to both Signal AND WhatsApp owner channels. Previously server.js sent startup/shutdown DMs to config.OWNER_UUID only, which routed exclusively to Signal — so when a restart was triggered from WhatsApp (e.g. self-modify after a WhatsApp-initiated plan), the owner saw confirmations only on Signal. New lib/users.js getOwnerChannelIds() reads the owner's users row (signal_uuid + whatsapp_id columns) from PostgREST with a 60s cache. New channels.broadcastToOwner(msg) fans out send() to each populated owner channel in parallel with independent .catch() per channel. server.js startup (intentional + unintentional branches) and shutdown notifications swapped from channels.send(OWNER_UUID, ...) to channels.broadcastToOwner(...). Owner identity sourced from DB, not env — no /config allowlist change required. |
| 16 May 20269:10 am |
koda-core |
2.111.4 |
Plan-driven restart notifications now mirror to both Signal AND WhatsApp. v2.111.3 routed cold-boot startup/shutdown DMs via channels.broadcastToOwner(), but plan-aware messages ('Step N/M Restarting…', 'Step N/M complete', rollback notices) went through two Signal-only paths: scripts/self-modify.sh notify_owner() (called scripts/signal-notify.sh) and the inline VERIFY_SCRIPT's notify() (POSTed directly to signal-cli /v2/send with hardcoded recipients=[OWNER_UUID]). New server endpoint POST /internal/broadcast-owner (localhost-only, mirrors /internal/notify auth) takes {message} and invokes channels.broadcastToOwner. scripts/notify.sh gains --channel both mode that POSTs to the new endpoint and, on Core failure, falls back to direct signal-cli /v2/send so the Signal arm survives a Core outage (WhatsApp arm dropped — WAHA fan-out is Core-only). scripts/self-modify.sh's notify_owner() and inline VERIFY_SCRIPT notify() both now invoke notify.sh --channel both. TDD: tests in tests/api-endpoints/internal-broadcast-owner.test.js, tests/scripts/notify-channel-both.test.js, tests/scripts/self-modify-channel-both.test.js (11 tests total). |
| 15 May 20266:30 pm |
koda-core |
2.111.2 |
Fix usermgmt bundle declaration mismatch. grant_capability and revoke_capability were declared in apps/usermgmt/manifest.koda.json but missing from bundles/usermgmt.yaml — the bundle gate at lib/intents.js:121 refused all dispatch attempts with 'action "grant_capability" is not declared in the usermgmt bundle'. After the 2.111.1 LID fix unblocked the verified_target gate, all 8 grants in Bent Street still failed at this next gate. bundles/usermgmt.yaml now declares both actions under actions: (chokepoint_fn=executeIntent, permission=manage_users; revoke_capability also destructive=true) and revoke_capability is added to top-level destructive_actions: to mirror the manifest. Bundle version 0.1.0 → 0.2.0. |
| 15 May 202612:45 pm |
koda-core |
2.111.1 |
Fix WhatsApp LID verification for identity-gated intents (grant_capability, revoke_capability, assign_role, etc.). When a WhatsApp user @-mentions a participant via the picker, WhatsApp substitutes the bare LID into the message body text — and WAHA may or may not also send mentionedIds metadata depending on engine. Previously only typed UUIDs and +E.164 phones entered verifiedIdentities, so LID-targeted intents from WhatsApp groups were rejected at the verified_target gate. server.js:1029 now adds a bare-LID regex (10-16 digits prefixed by @) channel-scoped to WhatsApp. Also rewords the verified_target gate error message in lib/intents.js:318 to name all three verified identifier types (UUID, +phone, LID) rather than mislabelling LID rejections as 'name-based references'. Triggered by Bent Street grant_capability failures (Klaus → Imogen) on 2026-05-04 and 2026-05-14. |
| 13 May 202610:30 pm |
koda-core |
2.111.0 |
Prompt-cache refactor (task #286). Split system prompt into stable + dynamic sections. Stable (identity, app context, code-change rules, group-type skill, instructions) goes to claude --append-system-prompt — preserves Anthropic's cached preset, byte-stable across turns so the prefix hits the 5-min prompt cache. Dynamic (sender capabilities, group members, memories, bundle matches, ambient backlog) is prepended to the user turn inside <system-reminder> tags so it can update per-message without busting the system-prompt cache. Also adds --exclude-dynamic-system-prompt-sections to move cwd/env/git noise into the first user turn. New exports: buildStableSystemPrompt(), buildDynamicPreamble(). Existing buildSystemPrompt() preserved for runWorkTask + invokeClaudeOneShot (lower-frequency callers, unchanged). Est. 70-85% prefix-token reduction on back-to-back Signal/web turns within the 5-min cache window. VS Code fallback path stays on the legacy monolithic prompt (no cache benefit anyway). |
| 12 May 202611:05 pm |
koda-transcribe-mcp |
1.1.1current |
Fix diarize.py for whisperx 3.8 API drift. Two surface-area changes: (1) DiarizationPipeline.__init__ kwarg renamed use_auth_token → token (matches upstream pyannote.audio.Pipeline.from_pretrained); (2) whisperx 3.8 attaches its own StreamHandler(sys.stdout) to the 'whisperx' logger with propagate=False, contaminating the stdout-JSON-only protocol — fixed by clearing+replacing the handler post-import to point at sys.stderr. Surfaced now because Koda Core's new /internal/cred endpoint finally lets the MCP resolve HF_TOKEN end-to-end, so we actually reached this code path for the first time. Verified: 55s 2-speaker m4a → 10 turns, 31s wall. |
| 12 May 202610:05 pm |
koda-core |
2.110.0 |
Add GET /internal/cred?domain=&username= endpoint so stdio-launched MCPs (koda-transcribe et al.) can resolve encrypted credentials on demand without bundling secrets in their launch env. Loopback-bind only, audit-logged on every call (action=internal.cred.read, channel=autonomous). Returns {password, value, domain, username} — password is canonical, value is a forward-compat alias. Backed by a new lib/cred.js helper (read-only getCred(domain, username) wrapping the existing PostgREST + lib/crypto.decryptCredentialAsync path). Unblocks diarization via koda-transcribe's HF token resolution; pattern is reusable for any future MCP needing a secret. Future hardening (shared-secret X-Koda-Internal header) deferred until MCP env propagation is solved. |
| 12 May 20266:55 pm |
koda-console |
1.39.17current |
Bug G round 12 — codify ios-pwa-keyboard as a shared module under apps/_shared/ via test-driven development. The four-layer canonical iOS PWA keyboard pattern (document-level viewport lock + transform-based counter-translation via --ios-pwa-app-offset CSS var + 500ms focusin RAF loop catching iOS's silent animation period + Chromium VirtualKeyboard forward-compat) is now a reusable JS+CSS module that every Koda Tier-2 PWA imports the same way. Built test-first: 24 tests green (10 pure-logic via node:test for isIOSPWA/computeViewportState + 14 DOM-driven via Playwright for activation, viewport sync, RAF loop timing, event dispatch, computed CSS rules). apps/console/js/app.js stripped of syncAppHeight/visualViewport plumbing — keeps only chat-scroll-anchor logic reacting to iospwa:viewportchange CustomEvent. apps/console/css/app.css #app rule simplified to non-iOS-PWA fallback. Revert tag: bug-g-round-11-baseline. |
| 12 May 20262:05 pm |
koda-console |
1.39.16 |
Bug G round 11 — flip #app back to top:0 + add visualViewport.scroll listener that calls window.scrollTo(0,0) on every scroll event to cancel iOS layout-viewport translation in real time. iOS WKWebView translates the layout viewport upward via WKScrollView.setContentOffset:animated:YES when the keyboard appears, even with body{overflow:hidden}. The scroll event fires throughout the ~250ms keyboard animation; scrollTo(0,0) cancels each translation frame, keeping #app pinned to screen top while its bottom edge compresses upward. Combined with height:var(--app-height) from vv.height, this matches the canonical chat-PWA pattern (saricden.com, codemzy.com, Siyuan PWA fix). Also adds interactive-widget=resizes-content to viewport meta (forward-compat for WebKit bug 259770; no-op on Safari today). Rounds 4-10 tried one of these in isolation but never the right combination. |
| 12 May 20261:21 pm |
koda-console |
1.39.15 |
Revert Bug G round 10 (CSS transition: height 0.2s on #app) — partial fix only; user reverted before deeper research. Round 9 bottom-anchor stays in place (it fixed the steady-state symptom of title bar clipped above visible area). The remaining keyboard-appear animation glitch (chat shell sliding up from screen bottom during iOS keyboard animation) requires a different approach — likely Round 11 will involve counter-translation via visualViewport.offsetTop with a debounce, or focus-event interception to pre-empt iOS's layout viewport translation. |
| 12 May 20261:10 pm |
koda-console |
1.39.14 |
Bug G round 10 — add `transition: height 0.2s ease-out` to #app to smooth the keyboard-appear animation. Round 9 (bottom-anchor) fixed the steady-state symptom but during the iOS keyboard animation, #app appears to slide up from the screen bottom because both visualViewport.height and the layout viewport's offset animate together. Round 10 lets the compositor smooth #app's height changes over ~200ms (matching iOS keyboard animation duration). If insufficient, revert and try counter-translation via visualViewport.offsetTop. |
| 12 May 202611:20 am |
koda-console |
1.39.13 |
Bug G round 9 — bottom-anchor #app (flip from top:0 to bottom:0, top:auto). Rounds 3-8 all anchored #app to the TOP of the layout viewport. iOS PWA standalone mode translates the layout viewport upward when the keyboard opens to bring the focused input into view; this translation fires even with `body { overflow:hidden }` because it's a layout-level translation, not a document scroll. Top-anchored #app follows the layout viewport up and off the screen — title bar clipped above visible area. Round 9 flips the anchor: bottom:0 with top:auto. The bottom of the layout viewport stays glued to the screen bottom (document can't translate past its end), so #app's bottom edge sits at the screen bottom and the composer (at the bottom of #app's flex column) lands above the keyboard. Title bar floats up at vv.height. Matches WhatsApp/Signal Web's actual chat-shell anchor pattern. Note: rounds 7 and 8 were behaviorally identical (both produced top:0) — only round 9 is a real structural change. --app-height tracking unchanged. |
| 12 May 202610:40 am |
koda-console |
1.39.12 |
Bug G round 8 — structural fix: drop --app-top tracking entirely (canonical chat-PWA pattern). Rounds 4-7 all tried to track or pin visualViewport.offsetTop and failed differently: r4 fed transient offsetTop spikes into #app top causing slides on open/close, r6 dropped the scroll listener but resize still fed transients, r7 pinned --app-top to 0 which broke the steady state (~60px residual offsetTop made #app top:0 of layout viewport land above visible area, clipping title bar). Root cause is iOS WebKit Bug #237851 (open since 2022, regressed in iOS 26): visualViewport.offsetTop is unreliable in PWA standalone mode during keyboard animations — no iOS version where it can be trusted. The canonical chat-PWA pattern (Signal Web, WhatsApp Web, Telegram Web) doesn't track offsetTop at all — they pin #app to top:0 of the layout viewport and ensure nothing inside extends past visualViewport.height. With no scrollable surface anywhere, iOS has nothing to auto-scroll and the residual offsetTop ghost disappears. Audit confirmed chat view structure is sound: #app-shell flex-col h-full, content flex-1, chat tab flex-col h-full with flex-shrink-0 header/banners/composer and flex-1 messages (overflow-hidden + inset-0 scrollable child). Round 8: removed the --app-top write in syncAppHeight, changed #app top from var(--app-top, 0) to top:0. --app-height tracking unchanged. |
| 12 May 202610:05 am |
koda-console |
1.39.11 |
Bug G round 7 — pin --app-top to 0 in syncAppHeight. Round 6 dropped the visualViewport.scroll listener and window.scrollTo(0,0) but the symptom persisted because the remaining resize listener was still feeding transient visualViewport.offsetTop values (~240px during iOS keyboard animations) straight into #app's `top:` property, causing the entire app shell to slide down then snap back on BOTH open and close transitions. Body is locked with overflow:hidden so the layout viewport equals the screen; offsetTop should always be 0 in steady state. Transient offsetTop spikes during keyboard animation are iOS WebKit noise. Round 7 stops trusting them — --app-top is now always written as 0px (still written so any stale value from a previous SW version is overridden on init). --app-height tracking via visualViewport.height is unchanged; that's what actually shrinks the shell to make room for the keyboard. The CSS fallback `top: var(--app-top, 0)` keeps the safety net. If the round 3 'title bar above visible area' flicker recurs in a future iOS version, round 8 will switch to bottom: 0 anchoring. |
| 12 May 20269:55 am |
koda-console |
1.39.10 |
Bug G round 6 — drop visualViewport.scroll listener and window.scrollTo(0,0) in syncAppHeight to break the iOS WebKit feedback loop that caused the title bar to slide off then reappear during keyboard transitions. The scroll event fired many times per frame during the keyboard animation; each fire called scrollTo(0,0), which fought iOS's automatic scroll-into-view for the focused input and produced the visible flicker. Body has `overflow:hidden; height:100%` so the document already can't scroll — the scrollTo was redundant. Keeping only the resize listener is sufficient — it settles --app-top and --app-height at the start and end of keyboard transitions. SW CACHE_VERSION bumped 1.39.9 → 1.39.10 (via auto-bump to 1.39.9-1, then explicit step to 1.39.10 to match this release). |
| 11 May 20269:25 pm |
koda-core |
2.109.5 |
Fix stale session names being carried across session resets. `sessions.name` was never cleared when `session_id` transitioned, so each new session in a group inherited the previous session's auto-generated title at archive time (via archiveSession copying `liveName = session.name` into the new session_history row, and the auto-name fallback being short-circuited because `liveName` was already truthy). Visible symptom: multiple distinct PWA sessions all titled the same — e.g. four 'Wandi App Command Architecture' entries in the chat sidebar with completely unrelated content (Ping/Pong probe, Gogolplex question, 2+2 repro probe). Two-edit fix in lib/sessions.js: (1) `resetSession` now clears `name: null` alongside `session_id: null` so the next session in the group starts blank; (2) `saveSession` detects a real transition (existing row's session_id differs from the incoming one) and clears `name` only then — never on heartbeat for the same session, because that would clobber the auto-namer's just-written title. Pre-existing mis-named session_history rows are not retroactively fixed by this change; that cleanup is tracked separately. |
| 11 May 20269:00 pm |
koda-console |
1.39.9 |
Fix Bug E — transient duplicate inbound bubble. pollChatMessages dedupes only by `timestamp > lastKnown`; optimistic client-pushed messages used a client-clock timestamp while DB rows got a server-clock timestamp a few ms later, so the server version passed the filter as 'new' and appeared as a duplicate bubble. Three changes in apps/console/js/app.js: (1) include `m.id` in all three /api/chat/history mappings (loadChat, pollChatMessages, post-send re-fetch) — the endpoint already returns `id` per lib/api/chat.js:507, the client was discarding it; (2) replace the timestamp filter with an id-Set check; (3) when a server message matches an id-less optimistic local entry by (role, content) at the tail, adopt the server id onto the existing entry instead of pushing a duplicate. SW CACHE_VERSION bumped 1.39.8 → 1.39.9 (via auto-bump to 1.39.8-1, then explicit step to 1.39.9 to match this release). |
| 11 May 20269:00 pm |
koda-core |
2.109.4 |
Fix Bug F — new threads showing 'Untitled' in the sidebar. Two compounding issues addressed: (1) lib/api/session-history.js hard-coded `name: null` for active (not-yet-archived) sessions, so any auto-named live session displayed 'Untitled' until archived — changed to `name: active.name || null` so the auto-name field flows through; (2) lib/logging.js had AUTO_NAME_THRESHOLD = 10 inbound messages, so short threads never got auto-named — lowered to 2 so the title generator fires after the first user→assistant exchange. Existing anti-hallucination filters in lib/sessions.js (line 757-809) handle short transcripts; the `_namedSessions` Set still guards against re-naming. |
| 11 May 20268:35 pm |
koda-console |
1.39.8 |
Revert #305 round 5 (1.39.7). Applying `position: fixed` to BOTH html and body collapsed the iOS PWA layout — composer at top, tab bar below it, messages area zero-height, large empty space above the keyboard. `position: fixed` on the html element is undefined/meaningless in WebKit and collapses the html box's effective height, cascading into broken layout. File-restored apps/console/css/app.css and apps/console/js/app.js to their 1.39.6 state (commit 9d7a1fa33) — html,body back to `height:100%; overflow:hidden`, #app back to `position: fixed; top: var(--app-top, 0); height: var(--app-height, 100dvh)`, syncAppHeight back to writing --app-top from visualViewport.offsetTop and pinning scroll with window.scrollTo(0,0). Bumped SW + version to 1.39.8 because 1.39.6 cannot be reused (clients have it cached). Bug G remains open — Test 2 cosmetic 'title bar slides off then reappears' flicker on iOS keyboard transitions still present, but the app is otherwise functional. Next attempt will use a different approach (candidates: track visualViewport.pageTop or window.scrollY instead of offsetTop; or apply position:fixed to body only — never to html). |
| 11 May 20267:55 pm |
koda-console |
1.39.7 |
Fix #305 round 5 — lock html+body with position:fixed so iOS Safari cannot scroll the document under us. Real root cause uncovered after rounds 3 and 4 failed Test 2 with the same 'title bar slides off then reappears' symptom: when an input is focused, iOS Safari has a special bypass that scrolls the document to bring the input into view, EVEN when body has overflow:hidden. position:fixed elements on iOS Safari ride along with document scroll (opposite of desktop behaviour), so #app at top:0 moved up with the scroll and the title bar slid off the top. visualViewport.offsetTop (round 4's --app-top knob) doesn't change for document scroll — only pinch-zoom — so chasing it via JS did nothing. The fix is to prevent the scroll from happening at all: add `position: fixed; width: 100%` to html+body. iOS respects this lock and won't scroll. #app becomes `position: absolute; top: 0` inside the locked body, sized by --app-height from visualViewport. Dropped the round 4 --app-top tracking and the window.scrollTo(0,0) pin — both made unnecessary by the body lock. Kept the chat-messages re-anchor on resize (still needed when --app-height shrinks the container). REVERTED in 1.39.8 — `position: fixed` on html collapsed the layout. |
| 11 May 20267:35 pm |
koda-console |
1.39.6 |
Fix #305 round 4 — track visualViewport.offsetTop and re-anchor chat on resize. Round 3 (1.39.5) tracked visualViewport.height into --app-height but missed that on iOS the keyboard offsets the visual viewport rather than scrolling the document. With #app at top:0, the title bar stayed anchored to the layout-viewport top while the visual viewport offset downward, so it appeared above the visible area (Test 2 flicker: 'slides off then reappears'). Also, when --app-height shrank the chat container, scrollTop didn't auto-adjust, leaving the latest message below the new visible area (Test 3: keyboard obscures latest message). Round 4 (a) writes window.visualViewport.offsetTop into a new --app-top CSS var, used as `top: var(--app-top, 0)` on #app — the shell now repositions into the visible area. (b) Probes #chat-messages scrollHeight − scrollTop − clientHeight before each resize; if user was within 100px of the bottom, re-anchors via requestAnimationFrame after the layout settles. Result: title bar stays put, latest message stays visible above the keyboard, scroll position preserved if user was reading older messages. |
| 11 May 20267:35 pm |
koda-core |
2.109.3 |
Fix(sw): drop the _swTemplate in-memory cache and the dead __CACHE_NAME__ substitution from /console/sw.js. The cache was invalidated by fs.watch, but macOS fs.watch silently dies on atomic file writes (Edit tools, git checkout, wrangler deploys all use rename-over-target writes). Once the watch died, the cached SW content went stale forever — required a Koda Core restart to flush. Symptom: a console SW bump landed on disk, was committed to git, but PWA users still got the old version until something restarted Core. Fix: read apps/console/sw.js per request. ~50µs disk read, SW is fetched once per session — perf cost is imperceptible. The __CACHE_NAME__ substitution was dead code anyway (sw.js has its own hardcoded CACHE_VERSION constant). Also added a try/catch with logger.error + 500 fallback so a missing/unreadable sw.js doesn't crash the response. |
| 11 May 20265:10 pm |
koda-console |
1.39.5 |
Fix #305 round 3 — proper keyboard-resize behaviour. Rounds 1 (1.39.3 `:first-child`) and 2 (1.39.4 `::before` margin-top:auto) both addressed the wrong symptom — they tried to anchor chat content to the bottom of the scroll area, which produced the wrong visual model (messages should flow top→down with empty space below) AND failed to fix the underlying iOS keyboard bug (title bar pushed off-screen when composer focused). Real root cause: `#app { position: fixed; top:0; bottom:0 }` is sized against the layout viewport, which doesn't shrink when the iOS keyboard opens — so the composer (at bottom:0) ends up below the keyboard, and iOS auto-scrolls the page to bring it into view, pushing the title bar off the top. Fix: drive #app height from `window.visualViewport.height` via a CSS variable (`--app-height`) set in `init()`, with resize+scroll listeners + `window.scrollTo(0,0)` pin to prevent iOS layout-viewport auto-scroll. When the keyboard opens, the visual viewport shrinks → `--app-height` shrinks → `#app` physically shrinks → flex children (header, messages area, composer) all proportionally adjust → bottom gap below the latest message compresses naturally → title bar stays visible. Reverts the ::before pseudo-element approach. Falls back to `100dvh` for pre-visualViewport browsers. |
| 11 May 20264:15 pm |
koda-console |
1.39.4 |
Fix #305 (round 2) — keyboard-open spacing actually working. The 1.39.3 fix used `#chat-messages > :first-child { margin-top: auto }`, but Alpine.js `<template x-for>` directives render real DOM `<template>` elements that are `display:none`. The :first-child selector matched the Alpine template placeholder instead of the first real message bubble, so the auto-margin had no layout effect — empty space stayed between the last message and the composer, and opening the keyboard pushed the status bar off-screen. Replaced with a `#chat-messages::before { content: ''; margin-top: auto }` pseudo-element approach: pseudo-elements are always part of the box's layout, unaffected by Alpine template hiding. Short threads now correctly stick to the bottom of the scroll area; keyboard-open compresses the gap naturally. SW CACHE_VERSION bumped 1.39.3 → 1.39.4 so cached assets invalidate. |
| 11 May 20264:15 pm |
koda-core |
2.109.2 |
Fix #304 follow-up — orphaned PWA inbound rows kept appearing AFTER the 2.109.1 column-typo fix landed. Two distinct root causes: (A) lib/api/chat.js captured `requestStartTime = new Date().toISOString()` AFTER the inbound logMessage() call, so the backfill PATCH's `timestamp: gte.<requestStartTime>` window excluded the very row it was trying to tag. Moved the capture above logMessage() — the row's PG-side NOW() now falls inside the gte window. (B) /api/chat/history with no `session_id` returned all messages in the group across all archived sessions, mixing prior threads into the current view (visible to user as their question landing in a previous chat's thread). Added a sessions-table lookup: when no session_id is passed, scope to the group's currently-active session_id. Falls back to unfiltered if the lookup fails so existing behaviour is preserved on error. TDD coverage in tests/api-endpoints/chat-backfill-column.test.js extended to assert the requestStartTime line precedes the inbound logMessage at source level. One-off repair script scripts/repair-orphans-2.js (using lib/postgrest chokepoint, no axios bypass) ran post-deploy: found 14 inbound orphans, repaired 1 (message 6759 'How much is a Gogol Plex' → session 91795fc3), the remaining 13 are legitimately session-less (security-test injections + reset turns). |
| 11 May 20269:25 am |
koda-core |
2.109.1 |
Fix #304 — newly-started PWA chats appearing under a previous thread's name. Root cause was a one-word typo introduced by commit 2772732ce on 2026-04-25 (originally a #273 fix). The /api/chat/send backfill PATCH on the `messages` table filtered by `created_at: gte.<requestStartTime>` — but the messages table has no `created_at` column; the actual column is `timestamp`. PostgREST returned 400, the wrapping try/catch in lib/api/chat.js silently swallowed it as a warn, and every PWA inbound message since 2026-04-25 landed with claude_session_id=NULL. That broke /api/sessions/list — inbound rows were invisible to session-based chat-list queries, the unfiltered fallback surfaced the previous session's metadata, and the user saw a new chat appear under an old thread's name. One-word fix in lib/api/chat.js line 337: `created_at:` → `timestamp:`. TDD coverage in tests/api-endpoints/chat-backfill-column.test.js asserts both the chat.js filter shape and the schema invariant (messages.timestamp exists, messages.created_at does not). One-off data-repair script scripts/repair-orphaned-claude-session-ids.js found 16 orphaned PWA inbound rows; 3 had a matching outbound within ±60s and were repaired, 13 were legitimately session-less (10 path-traversal security-test injections that never reached Claude, 3 turns the user reset/aborted before Claude responded). Repair script is idempotent — re-running shows 0 repairable. |
| 10 May 20269:11 pm |
koda-console |
1.39.3 |
Fix #305 — anchor short chat threads to the bottom of the scroll area. Previously, when a chat thread had only a few messages, empty space sat between the latest message and the composer. Tapping the composer opened the keyboard and shrank the visible area, but the gap was preserved instead of compressing — older messages stayed off-screen while blank space sat above the input. Fix uses the standard `margin-top: auto` on `#chat-messages > :first-child` flex pattern: short threads now stick to the bottom (empty space appears at the top), so when the keyboard shrinks the area the gap above compresses naturally and the latest message stays flush with the composer. When messages overflow, margin-top:auto resolves to 0 and normal scroll behaviour applies — existing scrollTop=scrollHeight calls still work. CSS-only change in apps/console/css/app.css; SW CACHE_VERSION bumped to 1.39.3 so cached assets invalidate on next visit. |
| 10 May 20263:50 pm |
koda-core |
2.109.0 |
Plan-routing remediation close-out (close of task #302). Closes the 14-task remediation queue spawned from the 2026-05-09 cross-channel plan-file corruption incident: channel-scoped self-modify via SM_PLAN_FILE (#3 + #3a, the actual P0 fix), age-capped startup-cleanup (#4 + #4a), proper-lockfile advisory locks + mutatePlan critical-section helper (#7), /internal/plan-continue handles WhatsApp LID/c.us/g.us shapes (#5), /stop reconciled with three-artefact keying (groupId for sessions, replyTarget for plan files, groupId for auto-queue) plus defence-in-depth for _work_task_* bg sessions (#6 + #10), /btw session-keying mirror (#11), /plan cancel imports fixed (#1), self-modify protocol prose updated so the LLM leaves steps in_progress and verify marks done (#8), plus three root-cause fixes added during the run: mechanical acceptance check on /work auto declared-Modifies files (#12), work-driven plans inherit mode:auto + workDriven flag (#13), and self-modify.sh skips /internal/plan-continue for workDriven plans so the work-loop owns resumption with no race (#14). docs/information-flow.md gained a new section consolidating the canonical plan-isolation model. Task #302 marked done with closing note. All 14 instruction files archived. Minor bump (not patch) because the remediation introduces new feature surface: SM_PLAN_FILE env-var protocol, age-cap behaviour at boot, advisory-lock semantics on plan writes, mutatePlan helper API, work-driven flag, and the mechanical acceptance check. |
| 10 May 20263:40 pm |
koda-core |
2.108.6 |
Plan-routing 10 (rewrite) — /stop defence-in-depth for background tasks. Original instruction file's premise was wrong: it claimed /stop got enqueued behind /work auto via the isGroupBusy gate, but audit 2026-05-10 confirmed slash commands dispatch at server.js:553-580 well before the queue gate at line 682 — /stop is never queued. The user's original 'stop didn't work' symptom from 2026-05-09 was the session-keying bug already fixed in #6 (commit 13a5ba271). However, audit surfaced a genuine defence-in-depth gap: lib/tasks.js (background research tasks) routes through runWorkTask, which keys sessions under `_work_task_<taskId>` in state.activeClaudeSessions. /stop's existing single-key lookup misses these entirely, so a user can't /stop a hung research task they started. This rewrite ships the defence-in-depth fix in lib/commands/stop.js: after the existing single-key SIGINT, iterate session map for `_work_task_<id>` keys, scope-check ownership via state.activeTasks (task.groupId match against sessionKey/planKey OR task.senderUuid match against context.senderUuid), and SIGINT each owned task. Reports count in the user-facing reply. 2 new unit tests in tests/lib-modules/stop-bg-task-defence.test.js (group-scoped + senderUuid-scoped ownership) — all 5 stop tests green. lib/commands/*.js is hot-reloaded — no restart needed. Old instruction file archived as instructions/archive/plan-routing-10-queue-fastpath-and-yield-OBSOLETE.md. |
| 10 May 20263:05 pm |
koda-core |
2.108.5 |
Plan-routing 11 — /btw session-keying mirror of #6. lib/commands/btw.js:39 used `const groupId = context.replyTarget || context.groupId` for its session lookup, but state.activeClaudeSessions and state.sessionCache are keyed on groupId (UUID) by lib/claude.js — not on replyTarget (phone). In Signal DMs replyTarget=phone but groupId=source-UUID, so the lookup always missed and /btw never forked from the active session — defeating the whole point of the command. Fix mirrors plan-routing #6 (stop.js): introduce `sessionKey = context.groupId || context.replyTarget` for the session-related lookups (activeClaudeSessions.has, sessionCache.get, getOrCreateSession), keep `groupId` for logger statements (lines 67/88/90 — user-facing identifier). 1 unit test in tests/lib-modules/btw-session-keying.test.js seeds activeClaudeSessions under the UUID, stubs sessionCache.get with a spy, asserts the spy was called with the UUID and never with the phone. TDD red→green confirmed. lib/commands/*.js is hot-reloaded — no restart needed. Closes the loop scoped out of commit 13a5ba271. |
| 10 May 20262:55 pm |
koda-core |
2.108.4 |
Plan-routing 7 — proper-lockfile around plan writes + new mutatePlan critical-section helper. lib/plan-utils.js writePlan and archivePlan previously did naked fs.writeFileSync with no advisory lock — two writers to the same file (LLM marking step in_progress while /continue adds _confirmShown, or verify script transitioning step status while LLM updates) could lose updates via classic read-mutate-write racing. #3a's channel-scoped matcher narrowed which file gets written but didn't eliminate the race within a single channel's plan. This change wraps writePlan and archivePlan with proper-lockfile.lockSync (sync API doesn't support retries natively, so lockSyncWithRetry handles backoff manually — 6 retries, 25-250ms exponential). New mutatePlan(groupId, mutator) helper holds the lock across read+mutate+write — this is the critical-section pattern code paths should use when updating an existing plan (not raw readPlan+writePlan). 1 unit test in tests/lib-modules/plan-write-locking.test.js spawns 10 concurrent worker_threads each marking a different step's _touched=1 — without the lock, ~3-8 mutations land; with mutatePlan, all 10 land. Test green. proper-lockfile@^4.1.2 added as new dependency. |
| 10 May 20262:40 pm |
koda-core |
2.108.3 |
Plan-routing 8 — reword self-modify protocol prose. lib/instructions.js previously instructed the LLM `**After each step**: Update the step's status to "done" (or "failed")` — the eager-done pattern that task #302 option 1 identified as breaking auto-continue post-restart. With the channel-scoped matcher landed in #3a (SM_PLAN_FILE pins the verify script to a single plan file), the eager-done fallback is no longer needed for plan-driven runs. The reword: keep `**Before each step**: in_progress`, instruct the LLM to NOT mark steps `done` itself (verify script does that after the post-restart health check), and explain why — (1) plan accurately reflects in-flight if restart hangs, (2) post-restart auto-continue can attach to the in_progress step with no brittle pending+file fallback, (3) atomic in_progress→done transition server-side. The LLM may still mark `failed` itself when it observes a hard failure verify can't detect (e.g. downstream test failure post-restart). 5 unit tests in tests/lib-modules/instructions-protocol-wording.test.js. All green. Closes the pure-prose half of #302 option 1; the mechanical half landed in #3a. |
| 10 May 20262:15 pm |
koda-core |
2.108.2 |
Plan-routing 4a — wire runStartupCleanup() into server.js boot, finishing #4's declared work. server.js boot was previously calling its own inline glob+archive logic against tmp/koda-plan-*.json: any plan whose surface metadata happened to be `every step done` was archived even if it was a fresh sibling from a parallel chat — the actual cause of cross-channel boot-archiving. The helper that #4 added (lib/plan-utils.runStartupCleanup) applies two-axis acceptance: archive only if all-done OR mtime > STARTUP_CLEANUP_STALE_HOURS (24h). Mixed-state foreign plans <24h are preserved. server.js now calls the helper inside a try/catch (boot must continue even if cleanup fails) and re-scans surviving files in a separate pass to compute planActiveOnRestart (still needed to suppress redundant 'online again' DMs during plan-driven restarts). 3 unit tests in tests/api-endpoints/server-boot-cleanup.test.js (calls runStartupCleanup + no inline archivedBy literal + require + reference). All green. Helper itself was already covered by tests/api-endpoints/startup-cleanup-scope.test.js. |
| 10 May 20262:00 pm |
koda-core |
2.108.1 |
Plan-routing 2a — finishing the half-done #2 (slugify-canonical) refactor. lib/api/chat.js now requires planFilePath from lib/plan-utils and uses it in the GET /api/plan endpoint, replacing the inline `path.join(planDir, koda-plan-${safe}.json)` template that #2 had named in scope but never touched. The archive block immediately below the planFile assignment keeps its manual archive logic (it derives the archive filename from path.basename(planFile) which works fine against the helper-built path). 3 unit tests in tests/api-endpoints/chat-plan-slug.test.js (no inline template + planFilePath called + planFilePath required). All green. Closes the inline-regex duplication called out in instructions/plan-routing-02a-chat-slug-wiring.md. |
| 10 May 20261:50 pm |
koda-core |
2.108.0 |
Plan-routing 3a — the actual P0 cross-channel-corruption fix from the 2026-05-09 incident lands. #3 (commit 8b9cb2ac4) only landed the helper module lib/plan-matcher.js; scripts/self-modify.sh continued to glob every tmp/koda-plan-*.json and match steps by `file === modifiedFile`, which is exactly the channel-blind logic that marked Signal #292 step 6 as done when the WhatsApp voice-note plan triggered a server.js restart. #3a wires pickStep into all four glob+match call sites (pre-restart, post-restart-in-verify-subprocess, rollback, commit-failed) with an SM_PLAN_FILE-scoped fast-path that pins the matcher to a single plan file. The verify subprocess block now passes SM_PLAN_FILE through to its env so post-restart matching honours the caller's plan. Legacy glob fallback retained in each site for the SM_PLAN_FILE-unset case (manual terminal invocations from operators). lib/instructions.js protocol prose (both the !isSelfModify default block and the isSelfModify==='execute' execution block) now instructs the LLM to emit `SM_PLAN_FILE="<plan-file>" bash scripts/self-modify.sh ...`, with a short note explaining the cross-channel scoping intent. 4 unit tests in tests/scripts/self-modify-channel-scoping.test.js (pickStep planFile-pin doesn't mutate foreign plans + self-modify.sh references SM_PLAN_FILE+plan-matcher + glob fallback retained + protocol prose includes SM_PLAN_FILE). All green. Plan-routing 3a closes the loop on the original P0 — the cross-channel corruption pattern is now structurally impossible when SM_PLAN_FILE is set, which the LLM now does by default. |
| 10 May 20261:30 pm |
koda-core |
2.107.1 |
Plan-routing 14 — scripts/self-modify.sh skips /internal/plan-continue when plan.workDriven===true (closes the work-continue/plan-continue duplicate-resumption race introduced by the matrix in #13). The auto-continue block's node-eval now surfaces three plan fields in one shot (mode + workDriven + groupId, tab-separated). When the plan is workDriven, slog logs 'Plan is workDriven — skipping plan-continue (work-continue handles resumption)' and skips the POST entirely. Non-workDriven plans (user-typed /plan auto, multi-step changes in normal chat) fire plan-continue exactly as before. Legacy plans without a workDriven field resolve PLAN_WORKDRIVEN=false → plan-continue fires as before (backward-compat preserved for in-flight plans created before #13 lands). 4 unit tests in tests/scripts/self-modify-resumption-routing.test.js (workDriven referenced + guards plan-continue + plan-continue POST still present + skip-log-line emitted). All green. Smoke-tested all three plan shapes (workDriven, legacy, no-pending-steps) via simulated bash routing — correct. No restart — script change exercised on next plan-driven restart. |
| 10 May 20261:10 pm |
koda-core |
2.107.0 |
Plan-routing 13 — work-driven plans inherit mode: auto + workDriven flag (root-cause prevention for the inner-plan-not-auto-continuing pattern). buildSystemPrompt gains a 12th positional `workDriven=false` parameter; when true, the plan-creation prompt template renders `"mode": "auto"` AND adds `"workDriven": true` to the example JSON, plus a one-line note explaining the work-loop owns post-restart resumption (no /continue prompt). lib/commands/work.js executeOneTask now passes `true` as the 12th arg, so any inner plan created during /work auto execution starts with mode auto and auto-continues across restarts without sitting waiting for a /continue that never came. Non-work plans (user-typed /plan, multi-step changes in normal chat) untouched — they keep default mode confirm and have no workDriven field. The workDriven field is the contract surface consumed by plan-routing #14 (next task) which will skip /internal/plan-continue when workDriven is true. 4 tests in tests/lib-modules/work-driven-plan-mode.test.js (default no-auto, workDriven=true forces auto + workDriven field, workDriven=false matches default, work.js source contains workDriven literal). All green. lib/instructions.js change requires restart; lib/commands/work.js is hot-reloaded. |
| 10 May 20261:00 pm |
koda-core |
2.106.0 |
Plan-routing 12 — /work auto mechanical acceptance check + partial status (root-cause fix for the 2026-05-09 partial-task incident). lib/commands/work.js now captures HEAD SHA before each task and runs `git diff --name-only <startSha>..HEAD` after Claude returns success. Parses `> **Modifies**:` from the instruction file (handles backticks, `(new)` annotations, 'None' placeholder). If any declared file is missing from the diff, the task is marked `partial` (⚠️ icon), the auto-loop halts, and the owner gets a notify listing the missing files. New helpers parseModifiesHeader() + verifyTaskAcceptance() + findNextTask exported for testing. findNextTask prereq check now accepts `partial` alongside `done` — semantics: a partial task means 'helper landed, wiring follow-up is valid', so Xa follow-up tasks (#2a/#3a/#4a) become eligible after their parent partials. 8 tests in tests/lib-modules/work-auto-acceptance.test.js (parser, verifier, partial-prereq, regression for failed/pending/done). No restart — lib/commands/* is hot-reloaded. Demonstration: would have caught #2/#3/#4 on 2026-05-09. The fix doesn't depend on prompt engineering, doesn't depend on restart fragility — it just looks at git, the source of truth. |
| 9 May 20268:30 pm |
koda-core |
2.105.2 |
Plan-routing 06 — fix /stop ↔ Claude session keying mismatch (AAR 2026-05-09 bug F). lib/commands/stop.js was looking up state.activeClaudeSessions by `replyTarget || groupId` (= phone number for Signal DMs), but lib/claude.js registers sessions by `groupId` (= source UUID). The Map.get() missed; only the plan-archive part of /stop ran while the Claude CLI kept streaming. Audit confirmed groupId is the canonical key across 8 of 10 readers (server.js queue gate, lib/api/chat.js, lib/api/groups.js, lib/api/sessions.js, lib/api/jobs.js, lib/commands/status.js); only stop.js and btw.js diverged. Fix: split stop.js's unified key into three per-artefact keys — sessionKey (groupId-canonical), planKey (replyTarget-canonical, matches lib/instructions.js's planFilePath(replyTarget) writer), autoQueueKey (groupId-canonical, matches lib/commands/work.js's autoStateFile(groupId) writer). Also fixes a parallel auto-queue lookup miss surfaced during the audit. New tests/lib-modules/stop-claude-session.test.js with 3 cases (Signal DM, Signal group/PWA where keys coincide, nothing-to-stop). All green. No restart — lib/commands/*.js is hot-reloaded. lib/commands/btw.js has the same bug but is scoped out of this task. |
| 9 May 20267:50 pm |
koda-transcribe-mcp |
1.1.0 |
First tracked release of koda-transcribe MCP in versions.json (was previously untracked at 1.0.0). Bumped from 1.0.0 → 1.1.0 to mark the diarize:boolean feature on transcribe_file/transcribe_url (whisperX + pyannote pipeline shelling out to scripts/diarize.py). package.json + src/index.ts version strings synced; dist/ rebuilt. No behaviour change vs the in-place 1.0.0 dist that already had diarize support — this entry exists so future MCP changes have a baseline to bump from. |
| 9 May 20265:30 pm |
koda-core |
2.105.1 |
Fix system_events metadata jsonb shape (follow-up to #292). lib/api/system-events.js.logSystemEvent() was passing metadata: JSON.stringify(metadata) to pgPost(); since pgPost JSON-stringifies the whole row before sending to PostgREST, the inner stringify produced a doubly-encoded payload and PostgREST stored metadata as a JSON STRING (jsonb_typeof = 'string') instead of a JSON OBJECT. Result: metadata->>'channel' and similar SQL operators returned null on every existing row. Fixed by passing metadata directly. One-time UPDATE migration converted 110 legacy string-typed rows to object-typed (UPDATE system_events SET metadata = (metadata #>> '{}')::jsonb WHERE jsonb_typeof(metadata) = 'string'). The GET /api/system-events handler retains its parse-on-read fallback (typeof e.metadata === 'string' ? JSON.parse(e.metadata) : ...) for safety. Verified end-to-end: restart events from this very commit (id=111 'pending', id=113 'verified') queried successfully via metadata->>'channel' = 'signal' / metadata->>'status' = 'verified' / metadata->>'commit_sha' matching HEAD. |
| 9 May 20264:30 pm |
koda-core |
2.105.0 |
PWA channel routing + restart event audit trail (#292). Closes the gap from #291: notify.sh --channel web with no explicit recipient now defaults to chat-${OWNER_UUID} so PWA-originated requests get progress notifications back via the PWA messages table + SSE/long-poll/Web Push (was previously falling back to bare OWNER_UUID, which Core treats as a Signal recipient). New POST /internal/system-event endpoint (server.js, localhost-only IP guard mirroring /internal/notify) wraps lib/api/system-events.js.logSystemEvent() so bash callers can write to system_events without JWT plumbing. scripts/self-modify.sh gains three log_system_event calls (pre-restart 'pending', post-verify 'verified', rollback 'rolled_back' / 'critical_failure') with metadata payload {channel, group_id, session_id, plan_step, file_modified, commit_sha, status, reason} read from the active plan file — restart events from plan-driven self-modifies now land in the PWA system feed alongside cold boot/shutdown. lib/instructions.js plan-creation prompt template gained a 'channel' field so self-modify.sh knows which channel originated the work for audit metadata. TDD throughout: tests/lib-modules/lib-instructions-plan-channel.test.js (4 cases for signal/whatsapp/web template assertions), tests/api-endpoints/internal-system-event.test.js (5 cases — endpoint exists, localhost guard, validation, logSystemEvent wired up), scripts/test-notify.sh extended with 2 new cases for PWA recipient defaulting (10 cases total, all GREEN). docs/information-flow.md gained 'Channel Routing & Restart Event Audit Trail' section documenting the matrix, metadata fields, endpoints, and DB schema. |
| 9 May 20261:25 pm |
koda-core |
2.104.0 |
Diarized meeting transcription, phase 1 of #303. New /meeting slash command (lib/commands/meeting.js, hot-reloaded) takes a single audio attachment in DM and returns a speaker-labelled transcript. Pipeline: lib/meeting-transcribe.js spawns mcp-servers/koda-transcribe/scripts/diarize.py (Python venv at .venvs/whisperx, whisperX 3.8.5 + pyannote-audio 3.1) and parses {speakers, turns, raw_transcript, duration_sec, language} JSON. HF token sourced from encrypted credentials store (domain=huggingface, username=token) via PostgREST + decryptCredentialAsync — never plaintext on disk. server.js gains NL detection: audio attachment + meeting/diarize/transcribe/who-said-what keyword in DM message text routes to the /meeting handler (Signal + WhatsApp, DM-only). koda-transcribe MCP gains a diarize:boolean option on transcribe_file/transcribe_url that routes through the same diarize.py. Capability gate: owner or `transcribe`. Phase 2 (DB persistence as scripts, /script command, label editing) deferred. |
| 9 May 202611:35 am |
koda-core |
2.103.0 |
Voice note transcription now works on WhatsApp DMs (previously Signal-only). server.js voice-note guard broadened from `channel === 'signal'` to `(channel === 'signal' || channel === 'whatsapp')`. WAHA already delivers audio attachments through the same attachmentList pipeline with `audio/*` content types, and lib/voice-transcribe.js is channel-agnostic, so this is a one-line scope expansion. DM-only still applies (groups remain unsupported). |
| 9 May 202611:10 am |
koda-core |
2.102.0 |
Channel-aware progress reporting + notify.sh wrapper (#291). New scripts/notify.sh routes notifications via Core /internal/notify (channel-agnostic) with a fail-loud fallback when Core is down and channel != signal; signal-only path still falls back to direct signal-cli. scripts/signal-notify.sh becomes a thin shim forwarding to notify.sh --channel signal so legacy callers keep working. lib/instructions.js progress-reporting blocks (conversational + self-modify execute paths) now name notify.sh and inject --channel <chan> from the existing channel function arg — silently mis-routing for WhatsApp users is fixed. TDD throughout: scripts/test-notify.sh shell harness (8 cases, single persistent mock) and tests/lib-modules/lib-instructions-channel.test.js (5 cases covering signal/whatsapp × conversational/execute/plan). Companion docs generalised: CLAUDE.md (Version Notifications, Pre/Post Change, When-to-checkpoint, params.verified), instructions/whatsapp-testing-and-cleanup.md, skills/self-modify.md, skills/deploy-dashboard.md (added WAHA :3038 to health check), skills/transcription.md (Signal Delivery → Channel Delivery with per-channel formatting/chunking notes). Lower-priority skills (research/web-page/web-control/etc.) deferred to a follow-up plan. |
| 9 May 20269:50 am |
koda-core |
2.101.0 |
Codify the canonical task-label set (#297). lib/commands/tasks.js now exports KODA_LABELS = [bug, feature, ux, maintenance, docs, infra, validation, deferred] plus a LABEL_ALIASES map for common synonyms (bugfix→bug, housekeeping→maintenance, polish→ux, refactor→maintenance, etc). /task add validates labels against the canonical set: unknown labels produce a warning + nearest-match suggestion appended to the 'Task created' reply, but the task is still created with the label as-is (override allowed per owner request). Apps and components are NOT labels — that's what scope/scope_id is for, so they're absent from the canonical set. Also fixed a misleading comment at the DM scope-resolution branch and corrected bundles/tasks.yaml: previous wording claimed '/tasks in DMs with no link → global tasks' which is not what the code does. Reality: koda_core has unrestricted RLS (USING(true)), so DM applies no scope filter and the Owner sees ALL tasks across global/group/app scopes, matching design intent. Latent gap captured as #301: non-Owner DM should filter to global + tasks they have rights to. Migration script scripts/migrate-task-labels.js applied to existing rows: 267/281 tasks rewritten to canonical labels, 2 scope flips (#259, #261 — usermgmt-tagged global tasks → scope=app). |
| 9 May 20269:35 am |
koda-console |
1.39.2 |
Hide #status-header (Koda logo, version pill, refresh button, profile avatar) on non-Home tabs (#298). Added x-show="activeTab === 'dashboard'" to the header so Chat/Tasks/More tabs gain the extra vertical room for content. Aligns with the per-tab visibility pattern used elsewhere in index.html. |
| 8 May 202610:10 pm |
koda-core |
2.100.1 |
Fix session auto-naming bugs (task #299). Bug 1 — every named owner session_history archive (56/56) was titled 'PWA Updates and Task Progress' regardless of actual content. Cause: generateSessionTitle() in lib/sessions.js invoked Claude haiku via invokeClaudeCode, which spawned the CLI from cwd=KODA_DIR and so loaded the project's CLAUDE.md / skills / MEMORY.md — the Koda-flavoured project context biased the model toward generic Koda-PWA-themed titles regardless of the conversation sample. Fix: (a) added optional systemPrompt param to invokeClaudeOneShot in lib/claude.js (passes --system-prompt); (b) generateSessionTitle now filters slash commands and short pings out of the sample, skips auto-naming when fewer than 3 meaningful messages remain (caller falls back to first-message truncation), routes through invokeClaudeOneShot with cwd='/tmp' so the project context can't load, and rejects known-bad hallucinated titles as a backstop. Bug 2 — every archived row showed started_at = 2026-03-10T10:42:54 (the date the sessions row was first created in the DB). Cause: archiveSession() copied session.created_at into session_history.started_at, but saveSession() reuses the row across resets, only PATCHing session_id + last_active — created_at never refreshes. Fix: archiveSession now derives started_at from the earliest message timestamp for the claude_session_id, falling back to session.last_active or now(). |
| 8 May 20269:48 pm |
koda-core |
2.100.0 |
Channel-aware system prompt (task #290): buildSystemPrompt() now takes a channel parameter and produces a channel-specific identity line — 'operating via Signal messaging' / 'operating via WhatsApp messaging' / 'operating via the Koda web console' / 'running as an autonomous /work task'. Previously hardcoded to Signal regardless of inbound channel, which caused Koda to confidently misidentify WhatsApp conversations as Signal. Wired through all 5 call sites: server.js (handleMention passes channel from envelope), lib/api/messages.js + lib/api/chat.js (web), lib/commands/modify.js + lib/commands/work.js (context.channel). |
| 8 May 20268:05 pm |
koda-core |
2.99.2 |
Fix WAHA auto-recovery — FAILED-state handler in server.js was calling whatsappStartSession() which silently no-op'd with 422 'already running' (because WAHA's RESTART_ALL_SESSIONS auto-spawns a session on stop), leaving the session FAILED indefinitely. Added whatsappRestartSession() chokepoint in lib/channels/whatsapp.js (calls /sessions/{name}/restart for an explicit stop→start cycle); FAILED webhook handler now uses it. Added 60s restart cooldown to prevent kicking restart on every webhook during outages, and a 30s post-restart status check that escalates 'needs re-pair (QR scan)' to the owner over Signal if WAHA is still FAILED — caught by a 10min cooldown to prevent alert spam during long outages. Today's incident exposed the bug: WAHA went FAILED at 07:06 UTC, auto-restart returned 422, marked 'success', session stayed FAILED for 2hrs until manual intervention. |
| 6 May 202612:05 pm |
network-diagram-skill |
0.2.1current |
SKILL.md roadmap update: Phase 3 (flow tracer) marked 'not built'. The IR JSON from Phase 1 already contains every routing/NAT/zone field needed to answer flow questions ad-hoc — no subcommand or codified path-walker required. Revisit only if ad-hoc answers prove insufficient. |
| 6 May 202611:50 am |
network-diagram-skill |
0.2.0 |
Phase 2 — sanitise subcommand. New 'netdiag sanitise <files...> [-o OUT | --in-place | --suffix SFX]' strips secrets from configs before they leave the network. Three format-specific redactors: OPNsense XML (catch-all tag patterns + explicit secret tags + LDAP DNs, all multi-line aware for cert/key blocks), Ubiquiti EdgeSwitch CLI (username/enable/line passwords, SNMP community + auth/priv keys, RADIUS/TACACS keys, RIP/OSPF key strings, dot1x), and Proxmox VE (qemu-server vncpassword/cipassword/cikeys/sshkeys, /etc/network/interfaces wpa-psk/wpa-passphrase, root@pam!name=UUID API tokens). Shared audit pass after every sanitise: long base64/hex blob detector + keyword sweep (passw/secret/psk/bindpw/api_key/token/community/shared_key) + XML secret-tag scanner. CLI exits non-zero if any residual secrets detected. Smoke-tested on 4 sandpit configs: 2 + 30 + 122 + 26 redactions, all audit-clean, build still produces identical topology (4 devices, 79 interfaces, 5 VLANs, 8 networks, 8 links). Rules ported from owner's two reference shell scripts. |
| 6 May 202611:30 am |
network-diagram-skill |
0.1.0 |
Initial release of the network-diagram skill (#70 in skills/index.json). Deterministic parser→IR→renderer pipeline (no AI in the diagram path). Parsers: OPNsense/pfSense config.xml (interfaces, VLANs, LAGGs, bridges, gateways, static routes, NAT, firewall rules, DHCP), Ubiquiti EdgeSwitch CLI (hostname, mgmt IP, vlan database, port PVID/tagged/untagged participation, LAG members), Proxmox /etc/network/interfaces + qemu-server VM configs. Cross-device link inference: explicit override > L3 shared subnet (0.9) > port-description hostname match (0.7) > VLAN trunk co-presence (0.4). Renderers: Mermaid (graph TB with role + zone subgraphs, classDef styling, fa: icons) and draw.io / diagrams.net mxGraph XML with Cisco shape stencils (mxgraph.cisco_safe.devices.firewall/switch/router, mxgraph.cisco.servers, cloud). CLI: netdiag build <files...> [-o OUT] [--override topology.yaml]. Smoke-tested on 4 sandpit configs (fcrsw01, fcrfw, prodfw, testfw) → 4 devices, 79 interfaces, 5 VLANs, 8 networks, 8 inferred links. Phase 2 (codified non-AI secrets scan + REDACTED) and Phase 3 (flow tracer: 'show me how my traffic gets to internet') deferred. |
| 6 May 20269:10 am |
koda-console |
1.39.1 |
Two regression fixes from 1.39.0. (1) Date input collapsed to a thin line because appearance:none stripped iOS Safari's native intrinsic height — added min-height:2.5rem and line-height:1.5 to input[type=date] so empty fields stay readable. (2) Pull-to-refresh fired inside the edit-task modal when the user dragged down to scroll — PullToRefresh._start and _move now bail when document.body has class 'modal-open' (already toggled by Alpine x-effect on every modal backdrop). Synced from apps/_shared/ to console/dashboard/det22/usermgmt. |
| 6 May 20268:45 am |
koda-console |
1.39.0 |
Three PWA bug fixes (#282, #287, #288). (1) #282: Chat rename toast no longer misfires on long sessions — startRenameSession now falls back to chatActiveSessionId when chatViewingSession.sessionId is missing (which can happen via SSE timing, sessionStorage roundtrip, or Vue reactivity quirks); only blocks if there are also no messages. (2) #287: Add Task and Edit Task forms now show only Title + Description by default; Priority, Visibility, Labels, and Due date collapse under a 'More' chevron. Tightened textarea padding for visually shorter editing area without dropping below 16px font (which would trigger iOS focus-zoom). Widened '+ custom label' input from w-24 to w-32 so the placeholder is no longer clipped to '+ custom labl'. Added -webkit-appearance: none on input[type=date] so iOS native picker chrome no longer paints over the right edge — rounded corners now apply uniformly. (3) #288: Task list is now hidden via x-show="!taskExpanded" while the create form is open, so its sticky header (sort dropdown + Select button) no longer peeks through below the Create Task button as 'two half buttons'. |
| 4 May 20269:30 pm |
koda-core |
2.99.1 |
Drop success-side audit log for POST /api/auth/token (lib/api/auth.js). CF Access already logs each SSO upstream, so the koda-core row was duplicate noise — and clients can stampede this endpoint after a restart (see koda-console 1.38.1 for the root-cause fix). Three plan-driven restarts earlier this hour produced ~12k success rows in 6 minutes from owner alone. The denied path remains audited so refused tokens stay traceable. Restart required for lib/api/auth.js. |
| 4 May 20269:30 pm |
koda-console |
1.38.1 |
Single-flight Auth.acquire() in apps/console/js/auth.js to kill the JWT-refresh thundering herd. When N concurrent API calls all hit 401 simultaneously (typical after a Koda Core restart), every one of them previously called Auth.refresh() independently and fired its own POST /api/auth/token — sustained ~70 req/sec bursts (6884 requests with 0ms gap) confirmed in audit_log. Wrapping acquire() with an in-flight promise collapses the cascade to one POST per refresh window. SW CACHE_VERSION 1.38.0 → 1.38.1 to push the new auth.js to clients. |
| 4 May 20267:15 pm |
koda-core |
2.99.0 |
WhatsApp identity-gate fixes + orchestrator hardening (six functional changes across five files). (1) lib/channels/whatsapp.js — normaliseMentions() now emits BOTH bare-digits and full @lid/@c.us forms for every WAHA mentionedId so the verifiedIdentities Set holds whichever form the orchestrator emits; new fetchGroupParticipants() pulls live group rosters from WAHA. (2) server.js webhook — wires normaliseMentions into envelope.dataMessage.mentions. (3) lib/sessions.js — new getGroupMembersForChannel(channel, groupId) returns a uniform {phone, lid?, display_name, isYou, isOwner} shape across Signal + WhatsApp; existing getGroupMembersFromSignal kept for back-compat. (4) server.js processMessage — replaces Signal-only roster fetch with channel-aware helper, so WhatsApp groups now get the anti-roster-resolution instruction block. (5) lib/instructions.js — _renderGroupMembersBlock rewritten: channel-agnostic wording ('Signal' / 'WhatsApp' inferred from member shape), LID column for WhatsApp members, strengthened anti-resolution rule explicitly forbidding name OR roster-identifier resolution into params.id for verified intents. (6) lib/intents.js — _ensureUserExists is LID-aware: detects 15+ digit identifiers (with/without @lid/@c.us suffix), looks up users via whatsapp_id (tries both bare + @lid forms), generates a fresh users.uuid + stores canonical @lid in users.whatsapp_id when auto-creating. New _resolveUserByIdentifier helper used by both grant + revoke paths. (7) server.js intent loop — gate-failure messages coalesced: rejections collected, grouped by (action, gate, error), sent as ONE consolidated ⚠️ per group ('8 × grant_capability rejected: <error>'); when ALL parsed intents in a turn are rejected, appends corrective line 'My previous message about that action was premature — nothing was actually executed.' to address the optimistic-narrative-then-gate-failure UX bug. Restart required for lib/sessions.js, lib/instructions.js, lib/intents.js, server.js. |
| 4 May 20267:55 am |
usermgmt |
1.9.0current |
Email-optional create_user. Migration 0004 adds phone + whatsapp_id columns + indexes on managed_users. POST /users handler now requires at-least-one of (email, phone, signal_uuid, whatsapp_id) instead of email-only — group-chat users can be created with just a phone or platform identifier. is_proxy decoupled from email-presence (now an explicit opt-in, no longer auto-set when email missing). Per-identifier uniqueness checks added. CF Access sync + welcome email gated on email presence directly. Manifest bumped to 1.2.0 with target_display_name + target_phone added to grant_capability/revoke_capability so auto-create-on-grant has human-readable identifiers. |
| 4 May 20267:55 am |
koda-core |
2.98.0 |
Auto-create users row on grant_capability when target isn't yet registered. _executeInternalCapabilityIntent in lib/intents.js now calls _ensureUserExists(uuid, displayName, phone) before writing to the permissions table — if the row doesn't exist, it inserts with role='viewer' (lowest floor) using target_display_name + target_phone from the @-mention metadata. Makes capability granting fully self-contained for group-chat users: 'give @Imo research and transcribe' in a chat now creates her users row + grants both caps in one batch, no separate create_user call needed. Restart was required for lib/intents.js. |
| 3 May 20268:30 pm |
koda-core |
2.97.0 |
Capability/permission/role cleanup + conversational cap-grant intents. Added new /caps and /whocan slash commands for capability introspection (lib/commands/caps.js, lib/commands/whocan.js — surfaced in /help). Added grant_capability and revoke_capability intents on the usermgmt manifest with internal_handler routing in lib/intents.js — they write directly to the Postgres permissions table via PostgREST and bypass the usermgmt CF Worker (Postgres is canonical until v3.0 Postgres→D1 sync lands). New params.context_inject manifest field auto-derives workspace_id from message context so the owner can say 'give @Name research' inside a group chat without specifying the workspace explicitly. Documented the capability/permission/role distinction in CLAUDE.md and docs/identity-action-security.md, including a workspaces-without-apps subsection. Restart was required for lib/intents.js. |
| 3 May 20264:05 pm |
koda-core |
2.96.2 |
Final approval-flow cleanup — drop AUTO_APPROVE_GROUPS from .env and .env.example. Was dead since v2.96.0 dropped state.AUTO_APPROVE_GROUPS in lib/state.js. No code path remains. No restart required (.env edits only). |
| 3 May 20263:55 pm |
koda-core |
2.96.1 |
Cleanup follow-up to v2.96.0 — drop the two dead env vars left behind by the approval-flow removal. Removed AUTO_APPROVE_DEFAULT and APPROVAL_TIMEOUT_MINUTES from lib/config.js (const declarations + module.exports), lib/api/config.js (SAFE_CONFIG_KEYS + UPDATABLE_KEYS allowlists), .env, and .env.example. No consumer remained after the v2.96.0 removal. Restart required (lib/config.js + lib/api/config.js). |
| 3 May 20263:25 pm |
koda-core |
2.96.0 |
Remove /approve, /deny, /autoapprove and all approval plumbing. The flow was dead code — Claude CLI is invoked with --dangerously-skip-permissions so the tool_use → onApprovalNeeded → /approve|/deny path never fired; audit_log confirms zero approval events ever recorded. Removed: lib/commands/approval.js, lib/commands/autoapprove.js, requestApproval() in server.js, POST /approve/:id endpoint, state.pendingApprovals, state.AUTO_APPROVE_GROUPS, state.AUTO_APPROVE_DISABLED, isAutoApprove/onApprovalNeeded params from invokeClaudeCode + handleClaudeEvent (lib/claude.js), pendingApprovals fields from /api/jobs and /api/status responses, /approve /deny /autoapprove from /help. Two restarts required (server.js + lib/* changes were spread across multiple commits). |
| 3 May 20262:35 pm |
koda-core |
2.95.2 |
/help now surfaces all registered slash commands. Added /listening (with /listen alias), /share, /pages, /research, /resume, /app, /workspace, /activate, /deactivate, /leave, /jobs, /btw, /wandi, and /deny — each gated by appropriate role/capability so non-owners only see what they can actually use. Hot-reloaded — no restart required. |
| 3 May 20262:30 pm |
koda-core |
2.95.1 |
Add /listen as an alias for /listening (group ambient context backlog command). Hot-reloaded — no restart required. |
| 3 May 20262:25 pm |
koda-core |
2.95.0 |
Two post-EIP bug fixes. (1) Scheduler path doubling — scripts/scheduled-jobs/weekly-memory-audit.js and daily-task-review.js used path.join(__dirname, '..') after being moved into scheduled-jobs/, resolving to scripts/ instead of the project root and producing the doubled '/scripts/scripts/signal-notify.sh' path that broke every scheduled run. Fixed by climbing two levels. Same root cause hit the .env path on both files — fixed in the same commit. (2) WhatsApp HMAC auth — /webhook/whatsapp was rejecting every WAHA delivery with HTTP 401 because the previous gate checked X-Api-Key (a header WAHA does not auto-forward on outbound webhooks). Replaced with HMAC-SHA512 verification matching WAHA's actual contract: signature in X-Webhook-Hmac, algorithm in X-Webhook-Hmac-Algorithm (read from header so any algorithm WAHA advertises is honoured), HMAC computed over the raw request body and constant-time compared via crypto.timingSafeEqual. Required mounting express.json with a verify callback as a path-prefix middleware on /webhook/whatsapp BEFORE the global json middleware (otherwise the global consumes the stream and rawBody stays undefined). Added new env var WAHA_HMAC_KEY (Koda side) and recreated the WAHA container with WHATSAPP_HOOK_HMAC_KEY (WAHA side); 32-byte random hex shared secret never crosses the wire. Graceful degradation: if WAHA_HMAC_KEY is unset, accept with a warning to avoid chicken-and-egg lockout during config rollout. End-to-end verified: valid sig → 200, tampered body → 401, missing header → 401, real WAHA session.status delivery processed cleanly. Restart required. |
| 3 May 20261:30 am |
koda-core |
2.94.1 |
Calendar bundle auto-subscribe fix (REM-04 follow-up). When the owner shares a Google Calendar with the service account, the share grants ACL access but does NOT add the calendar to the SA's calendarList. Verified against Google Calendar API docs (developers.google.com/calendar/api/v3/reference): no API path enumerates calendars-with-ACL-but-no-subscription for a SA — calendarList.list, calendars.get, acl.list, freebusy.query all require a known calendarId or pre-existing subscription. Fix: hybrid persistent-cap + ad-hoc-subscribe. (1) New env var KODA_CALENDAR_SUBSCRIBE_IDS — comma-list of calendar IDs the owner has shared. listCalendars() diffs that env var against the SA's current calendarList on every call and inserts any missing IDs (one shot per ID, audit-logged, failures swallowed so a stale entry never breaks the listing). (2) New bundle action subscribeCalendar({calendarId, requestor}) — for ad-hoc additions when the owner shares a new calendar. Inserts the ID, audit-logs the action, AND appends it to KODA_CALENDAR_SUBSCRIBE_IDS in .env (and process.env) so future restarts replay the subscription — no /config dance needed. Idempotent. (3) bundles/calendar.yaml updated: removed the wrong 'missing = not shared' guidance, added 'Calendar visibility — sharing vs subscribing' section explaining the API gap, registered subscribe_calendar action with worked Bash example, documented the env var. (4) lib/commands/config.js — KODA_CALENDAR_SUBSCRIBE_IDS added to CONFIGURABLE_KEYS. (5) tests/lib-modules/lib-calendar.test.js — 5 new auto-subscribe tests added (12 total, all pass): missing-ID auto-subscribe, no double-subscribe, idempotent ad-hoc, env persist, failed-insert-doesn't-break-list. Smoke confirmed end-to-end: 5 calendars now visible (Koda + [email protected] + Friendship dates + Wandiligong + Family). Restart required. |
| 3 May 202612:50 am |
koda-core |
2.94.0 |
EIP remediation bundle 2 — closes REM-05 + REM-06 + REM-07 + REM-09 + REM-11 + REM-12 + REM-13 + REM-14 (8 of remaining 9 queue rows). (REM-05 / F-P2-A) tests/security/intents-dispatch.test.js — 18 hermetic behavioural tests covering every refusal path of checkBundleGate, validateIntent gates A/B/C, executeBatch destructive hold + post-confirm dispatch. Red-green verified. (REM-06 / F-P2-B + F-P2-C audit.js) lib/audit.js registered as code-enforced canonical writer via new owns_table_writes field; eslint catches direct pgPost('audit_log',...) outside lib/audit.js. lib/logging.logAudit kept as thin shim (avoids cascading migration of ~30 callers); lib/sync.collectAuditLogs migrated to audit.logAction. Hermetic L4-fallback tests added. (REM-07 / F-P2-C remaining 6) registered keysvc-client, auth-headers, usermgmt-client, sync, file-share, integrations chokepoints with appropriate owns_urls/owns_config_constants/owns_table_writes; 22 documented_bypasses for legitimate setup-script and boilerplate references. (REM-09 / F-P2-E) backfilled verification boxes in 13 archived EIP instruction files (45 ticked with commit refs, 15 marked NOT VERIFIED at archive time); sub-queue status note added. (REM-11 / F-P3-B) removed broken $schema reference from chokepoints.json. (REM-12 / F-P3-C) confirmed already addressed pre-audit (server.js routes through channels.whatsapp.getGroupSubject). (REM-13 / F-P3-D) CLAUDE.md Pre/Post Change Notifications section now distinguishes Signal notifications (always) from pre-edit checkpoint commits (only for risky/multi-step/restart-causing/>60s changes); 26% noise commits no longer mandated. (REM-14 / strand-8 caveat) tests/lib-modules/lib-claude-shutdown.test.js — 2 fake-timer tests proving the 30s force-resolve fallback fires when SIGKILL is ignored, and the double-resolve guard works. Red-green verified. Restart required. |
| 3 May 202612:30 am |
koda-core |
2.93.0 |
EIP-REM-04 — wire calendar execution path (bundle-as-docs pattern). Calendar wiring is the proof of the EIP pattern; per owner Q&A 2026-05-02 it is system infrastructure, not a Tier-2 app — no /calendar slash command, no apps/calendar/manifest.koda.json. Claude invokes lib/calendar.js via Bash + node -e using the calling pattern documented in bundles/calendar.yaml. Changes: (lib/calendar.js) added _writeAllowList/_assertWriteAllowed enforcing KODA_CALENDAR_WRITE_ID + KODA_CALENDAR_WRITE_IDS allow-list on createEvent/updateEvent/deleteEvent (fail-closed when unset; error message names offending calendar AND current allow-list). Audit-log calls already wired pre-existing on every entry point. (bundles/calendar.yaml) doubled in length with bundle-as-docs guidance: explicit Bash invocation pattern (cd /Users/koda/koda && node -e "..."), allow-list documentation, two worked examples (create-event + list-events with full RFC3339 Melbourne timezone), confirmation rules for destructive actions, anti-patterns. (lib/commands/config.js) added KODA_CALENDAR_WRITE_ID and KODA_CALENDAR_WRITE_IDS to CONFIGURABLE_KEYS so owner can set them via /config set from Signal DM. (tests/lib-modules/lib-calendar.test.js) 7 cases covering empty allow-list refusal, single-id, comma-list, combined, error-message contents, whitespace tolerance. Live verification: GCP creds + Klaus's Koda calendar (`[email protected]`) reachable from CLI; full Signal end-to-end smoke test is owner-driven (Klaus sets KODA_CALENDAR_WRITE_ID then says e.g. "what's on today"). Restart required. |
| 2 May 202611:55 pm |
koda-core |
2.92.0 |
EIP remediation bundle — closes REM-01 + REM-02 + REM-03 + REM-08 + REM-10 (5 of 14 queue rows). (REM-01 / F-P1-B) restore bundle matcher to on-demand intent: lib/instructions.js gates full bundle-self-check skill body on bundleMatches.length>0 (1,217-token full body becomes ~60-token stub on no-match turns); lib/bundles/matcher.js re-weights scoring (ACTION_SCORE 2→1, DEFAULT_THRESHOLD 2→3, DEFAULT_MAX_MATCHES 5→3). New tests/lib-modules/lib-bundles-matcher.test.js (8 cases) prove the audit's pathological action-soup message no longer pulls 5 bundles. (REM-02 / F-P1-C) close ESLint config-constant blind spot: new `owns_config_constants` field on chokepoints.json entries; eslint.config.js generates a per-owner no-restricted-syntax rule banning `config.X` outside the owning chokepoint module. Surfaced 18 violations including 4 previously-unknown server.js findings. (REM-03 / F-P1-D) routed 8 call sites via chokepoint helpers — added signal.getReceiveWebSocketUrl/health/quitGroup/listGroups/sendReceipt/fetchAttachmentBuffer, whatsapp.isConfigured/leaveGroup/listGroups, claude.fetchAvailableModels/runWorkTask/runBtw (with shared concurrency gate extracted from invokeClaudeCode — runWorkTask now participates), postgrest.health. (REM-08 / F-P2-D) lib/channels/index.js receipts + lib/attachments.js attachment-fetch routed via the new signal helpers. (REM-10 / F-P3-A) lib/commands/status.js consumes integrations.checkAllHealth() filtered by chokepoint module name. server.js bypasses fixed in commit f9d11fe (separate per CLAUDE.md). Restart required. |
| 2 May 202610:25 am |
koda-core |
2.91.1 |
Fix critical stuck-queue bug — Claude CLI escalating shutdown. Root cause: lib/claude.js sent SIGTERM 10s after the result event when the CLI didn't exit cleanly, but if the child had spawned MCP server children that hadn't closed their stdout pipes, child.on('close') never fired. The outer Promise hung forever, server.js's finally block (which calls messageQueue.releaseGroup) never ran, and subsequent Signal messages queued silently behind a dead claim. Reproduced in core-2026-05-01.log at 22:57:14Z — three messages (Anything left?, What are you doing, /usage) were queued and dropped before /reset wiped state. Fix: escalating shutdown chain — 10s SIGTERM (graceful), 20s SIGKILL (hard, MCP children may be blocking wait()), 30s force-resolve the Promise (destroy stdio, unref child). The 30s force-resolve is the safety net: even if SIGKILL doesn't produce a close event (zombie process, stdio inherited by an unkillable subprocess), the group claim still gets released and Signal messages keep flowing. Added `resolved` flag in close/error/timeout/force-resolve paths to prevent double-resolve on the outer Promise. Watchdog timers tracked in `watchdogTimers[]` and cleared on close/error so they don't leak. Defense in depth — the previous code path handled SIGTERM but not the case where SIGTERM was ineffective. |
| 2 May 20269:30 am |
koda-core |
2.91.0 |
EIP §5 cleanup batch 3 — four findings closed in one auto-mode plan (M3, L1, L5, L6). 13 steps, 1 Koda Core restart, 3 Tier 2 worker deploys, 1 VS Code Bridge rebuild + vsix install. (M3) New shared module apps/_shared/worker/rate-limit.js — pure-JS in-memory token-bucket factory rateLimit(key, capacity, refillPerSec) → { ok, retryAfterSec }. Wired into authenticateAdmin in all three Tier 2 worker middlewares (det22, usermgmt, koda-dashboard) with capacity=30, refill=0.5/s (30/min). Keyed by CF Access common_name when present, else hash of the bearer token. On refusal: 429 with Retry-After header. Verified end-to-end via HTTP/2 multiplex burst tests (32 calls in <1s → first 30 succeed, last 2 return 429). Per-isolate (V8) bound — fine for single-instance Workers; multi-isolate scaling would need a Durable Object/KV upgrade later. (L1) koda-vscode-bridge RunCommandTool.exec gained COMMAND_DEFAULT_ALLOWLIST (git, npm, node, ls, pwd, cat, grep, rg, bun, wrangler) + COMMAND_DENYLIST_SUBSTRINGS (rm -rf, sudo, curl, wget, nc, ssh, dd, format, > /dev/). Denylist checks first (substring match anywhere in command), then allowlist by basename of first token. KODA_BRIDGE_ALLOWED_COMMANDS env var overrides the allowlist. Refusal logs to outputChannel + returned to caller as toolResult.error. vscode-bridge bumped 0.2.5 → 0.3.0; vsix built + installed (window reload required to activate). (L5) /status gained a 'claude-settings' subsection (owner-only) reading ~/.claude/settings.json (graceful on missing/unreadable) and reporting permissions.defaultMode, skipDangerousModePermissionPrompt, model, autoUpdatesChannel, plus the constant '--dangerously-skip-permissions' flag from lib/claude.js. Module-load-time one-shot audit emission 'claude.dangerous_settings' if any flag is dangerous (suppressed via module-level seen flag for repeats). Hot-reloaded. (L6) New chokepoint lib/playwright.js — exports launch(opts), launchPersistent(profileDir, opts), createContext(browser, opts). Owns: stealth-plugin attach (lazy require, only when needed via { stealth: true }), default args ['--no-sandbox', '--disable-setuid-sandbox'], profile dir resolution (explicit > PLAYWRIGHT_PROFILE_DIR env > ~/.koda/playwright-profile), default viewport 1280x800, SingletonLock cleanup before launchPersistentContext, fire-and-forget audit row emission (action='playwright.launch', detail=callerHint or stack-walked caller). Migrated all 7 known callers (scripts/web-control.js, scripts/pw-browse.js, lib/usage.js, lib/document-generator.js, scripts/wandi-control.js, scripts/render-diagrams.js, scripts/open-browser.js) — re-grep confirms zero direct 'playwright' imports outside the chokepoint. Registered in docs/chokepoints.json. ESLint flat config auto-generates no-restricted-syntax rule from the manifest — verified to fire on bypass attempts. Plan executed in auto mode. |
| 2 May 20268:53 am |
vscode-bridge |
0.3.0current |
EIP §5 L1 closure — RunCommandTool.exec gained command allowlist + denylist gates. COMMAND_DEFAULT_ALLOWLIST = ['git','npm','node','ls','pwd','cat','grep','rg','bun','wrangler'] (basename of first token must match). COMMAND_DENYLIST_SUBSTRINGS = ['rm -rf','sudo','curl','wget','nc ','ssh ','dd ','format','> /dev/'] (substring match anywhere in command — checked first, overrides allowlist). KODA_BRIDGE_ALLOWED_COMMANDS env var overrides the allowlist (comma-separated). Refusal logs to bridge outputChannel AND returns error to caller as toolResult.error. vsix built + installed via 'code --install-extension --force koda-vscode-bridge-0.3.0.vsix' — VS Code window reload required to activate runtime. Closes 'bridge trusts whatever VS Code's language model asks for' finding. |
| 1 May 202611:30 pm |
usermgmt |
1.8.3 |
EIP §5 M3 closure — admin endpoints rate-limited at 30/min per principal via shared apps/_shared/worker/rate-limit.js token-bucket. authenticateAdmin in src/worker/middleware.js calls rateLimit(principalKey, 30, 0.5) after successful auth (CF Access common_name match OR bearer match path). principalKey = CF common_name when present, else SHA-256 hash of bearer token. On refusal: returns { ok: false, rateLimited: true, retryAfterSec, error: 'rate limited' } from middleware; admin.js routes that to a 429 Response with Retry-After header. Verified via HTTP/2 multiplex burst test: 32 sequential calls in <1s → first 30 succeed (200), last 2 return 429 with Retry-After. Per-isolate (V8) bound; sufficient for single-instance Worker. Worker version a59c5d68-2cc5-4e76-90ee-5f03d4aa4a91. |
| 1 May 202611:15 pm |
koda-dashboard |
1.3.2current |
EIP §5 M3 closure — same rate-limit wiring as det22/usermgmt. Per-principal token bucket on admin endpoints (30/min, refill 0.5/s). 429 mapping in admin.js applies to BOTH the optional-auth /health path and the auth-required paths. Verified via burst test (33 calls → 30 OK + 2 × 429 with Retry-After). With this and the prior 1.3.1 EXPECTED_SERVICE_TOKEN_CN landing, koda-dashboard's worker now matches det22/usermgmt for auth + rate-limit posture. Worker version 3608fd5c-7d3a-49b7-b70e-c1b35e8dc8b4. |
| 1 May 202611:00 pm |
det22 |
1.10.3current |
EIP §5 M3 closure — same rate-limit pattern as usermgmt 1.8.3. authenticateAdmin wraps the admin endpoint with token-bucket gate (30 calls / 60s, refill 0.5/s) keyed on CF Access common_name or bearer hash. 429 + Retry-After mapped at the admin.js handler boundary. Verified via burst test (32 calls → 30 OK + 2 × 429). Worker version 0ef34758-e2f3-49b2-a83a-1c7a9d50c4e2. |
| 1 May 202610:48 pm |
koda-core |
2.90.0 |
EIP §5 cleanup batch 2 — five Medium/Low items closed in one auto-mode plan, four restarts + one docs-only commit. (M4) lib/voice-transcribe.js no longer hard-codes /opt/homebrew/bin/whisper-cli, /opt/homebrew/bin/ffmpeg, /Users/koda/mcp-servers/models — replaced with WHISPER_BIN / FFMPEG_BIN / WHISPER_MODELS_DIR env vars defaulting to the existing paths so behaviour is unchanged on this host but moving to another machine becomes a config change, not a code change. (M9) lib/file-share.js gained a checkShareAccess(ip, share) gate with per-IP token-bucket rate limiting (SHARE_RATE_MAX_PER_IP=30 over 60s) and per-token download cap (SHARE_TOKEN_DOWNLOAD_CAP=100). server.js /share/:token route now calls the gate before resolveCorefile; refusal returns HTTP 429 with Retry-After + audit action='share.rate_limited' (reason=ip_rate or token_cap). Both bounds env-overridable. (M10) lib/api/chat.js fixed the PWA confused-deputy: ALLOWED_TRANSPORTS=['','pwa','signal'] enforced via validateTransport() helper applied to /send, /command, /plan, /active. Refuses unknown values with HTTP 400 + audit 'api.chat.cross_channel_refused'; audit-logs every signal-transport request with 'api.chat.cross_channel' for visibility (not denial — PWA legitimately uses transport='signal' to resume Signal sessions). (L2) lib/api/auth.js /token/service now supports CF_OWNER_SERVICE_TOKEN_CN env-or-config allowlist (comma-separated common_names). When set, after the existing client ID/secret match, also requires inbound Cf-Access-Jwt-Assertion whose common_name claim is in the allowlist. Backward-compat: env unset preserves prior behaviour. Mismatch returns 401 + audit 'api.service_token.cn_mismatch'. (M6) docs/key-rotation.md added — 335-line runbook for rotating the keysvc credential key: why rotate, preconditions (off-peak, Koda Core stopped, key + DB backup), procedure (decrypt-all → atomic key-file swap → re-encrypt → verify → restart), rollback paths for each phase, post-conditions, what's NOT covered (cipher migration, multi-key keysvc, key escrow). Closes the 'no documented rotation procedure' part of M6 — actual rotation script remains an at-rotation-time build per the runbook. Plan executed in auto mode. |
| 1 May 202610:05 pm |
koda-core |
2.89.0 |
EIP §5 cleanup batch 1 — four Medium/Low items closed in one auto-mode plan, three restarts + one hot-reload. (M5) lib/file-share.js shareFile() now resolves and validates the source path against an explicit allowlist (SHARE_ALLOWED_ROOTS = [config.KODA_DIR]) before sharing — the path-traversal guard moved out of the command layer (lib/commands/share.js) into the chokepoint itself, so any future caller of shareFile() inherits it. fs.realpathSync resolves symlinks; prefix-match against the allowed root rejects anything outside. Defence-in-depth: command-layer check retained. (M7) lib/sync.js getAdminApiKey() previously read .secrets/admin_api_keys from disk on every call. Now caches Map<appName, key|null> with lazy fs.watch invalidation: watcher registers on first call after a successful read, clears the cache on file change, gracefully self-cleans on watcher errors. Caching null is intentional (avoids re-reading for apps with no admin key). Cache size bounded by Tier 2 app count (~3). (L3) lib/usermgmt-client.js _fetch() now wraps requests with 3-attempt exponential backoff (250/500/1000ms) for network errors and 5xx; 4xx surface immediately so auth/permission errors aren't masked. assignRole() refactored to route through _fetch via new extraHeaders param (X-Directed-By), unifying the retry path. lib/sync.js fallback to direct Postgres push remains as last-resort. (L7) lib/commands/cred.js /cred reveal now prepends an auto-expiry warning with the 60s countdown and schedules a follow-up 'reveal expired — delete the message above' notice. signal-cli-rest-api 0.98 (json-rpc mode) doesn't expose remote-delete via REST (probed /v1/rpc → 404, v2/send capabilities only quotes+mentions), so this is a soft time-box; the real defence remains owner-only + DM-only + audit-logged. Hot-reloaded. (M1, L4 doc-stale) Marked resolved in audit doc — M1 satisfied by docs/chokepoints.json + lib/integrations.js + /status integrations from EIP Week-2 #12; L4 satisfied by EIP #10 Layer A buffer + Layer B drain + stuck-buffer DM. Plan executed in auto mode. |
| 1 May 20267:58 pm |
koda-dashboard |
1.3.1 |
EIP Q1 #20b closed — wrangler.toml [vars] now declares EXPECTED_SERVICE_TOKEN_CN = 'c2ce2792eb99295cd3e30ea40acf673f.access' (the koda-dashboard CF Access service-token client_id). Worker redeployed; live env binding confirmed. authenticateAdmin() in src/worker/middleware.js validates Cf-Access-Jwt-Assertion.common_name against this value, blocking cross-app spoofing. Smoke tests pass: (a) matching service token → handler-level 400 (auth passed); (b) det22 service token → 401 with 'service token mismatch'; (c) bearer fallback retained. Per-app identifier is public, not secret — [vars] is the right home (the matching secret half lives in the cred store on Koda Core, domain 'cf-access-koda-dashboard'). Version a5928783-bc74-4698-ad94-7068bafb9b9c. |
| 1 May 20267:58 pm |
koda-core |
2.88.0 |
EIP backlog cleanup — three Week-1/Week-2 items closed in one auto-mode plan, two restarts. (#10 Week-1) Layer-B audit-pending drainer landed at scripts/scheduled-jobs/audit-drain-watchdog.js: atomic-renames logs/audit-pending.jsonl to .draining.<ts> so concurrent appendFile from lib/audit.js lands in a fresh file, POSTs each row to PostgREST audit_log, rotates to .applied.<ts> on full success (cleaned up after 7 days), appends failed rows back to live file otherwise. Stuck-buffer alarm DMs the owner if the live file is >30min stale OR >100 lines, with 30min re-alert suppression via tmp/audit-drain-state.json. Registered as scheduled_tasks row id=8 'Audit Pending Drain', cron */5 * * * *. Documented bypasses added to docs/chokepoints.json (postgrest, signal — must work when Koda Core is down). (#14 Week-2) lib/inbound-anomaly.js inspectInboundMessage was sitting unwired since landing; server.js:processMessage now calls it fire-and-forget right after messageText is finalized — never blocks, never mutates, never throws to the caller. Hits land in audit_log with action='inbound.anomaly' and DM the owner if a non-owner sender trips a rule. (#16 Week-2 / §5 M8) Defence-in-depth on top of ALLOWLIST_ROOT: scripts/scheduler-allowlist.txt enumerates 7 active scripts (anomaly-watchdog, audit-drain-watchdog, daily-task-review, reconcile-apps, reset-timeout, watchdog, weekly-memory-audit). lib/scheduler.js loads the file at module init into a Set; executeTask() refuses any script-type task whose basename isn't in that set with action='scheduler.task.refused', reason='not in allowlist' — same audit + owner-DM path as the existing dir-out refusal. Not hot-reloadable (cron map is in-memory) — full restart required to pick up edits. Two restarts (server.js step, lib/scheduler.js step). Plan executed in auto mode. |
| 1 May 20266:10 pm |
koda-core |
2.87.0 |
EIP audit CRITICAL/HIGH cluster — H1 + H3 + H8 closed; C1 confirmed already resolved (audit was stale at write time). Eight-step plan, three restarts. (H1) lib/scheduler.js:_executePrompt was spawnSync('claude', ['-p', '--output-format', 'json'])'ing directly, skipping the lib/claude.js chokepoint (no concurrency gate, no /stop interruptibility, no model selection). Replaced with await invokeClaudeOneShot(prompt, {outputFormat:'json', timeout, cwd, env:{KODA_SCHEDULED:'1'}}); session-id capture preserved for the expects_session guardrail; net -19 lines, dropped spawnSync from imports (commit 31764c9). (H3) The remaining WAHA bypass — server.js:1534 axios.get(${WAHA_BASE_URL}/api/default/groups/${id}, {X-Api-Key, timeout:5000}) for fetching a group's display name — was migrated. lib/channels/whatsapp.js gained async getGroupSubject(groupId) that wraps the WAHA group endpoint with wahaHeaders() auth and 5s timeout (commit 10a6402, hot-reloaded). server.js now calls await channels.whatsapp.getGroupSubject(waFrom) and reads .subject; orphaned WAHA_BASE_URL constant deleted; -9 lines server.js (commit bd1592a, restart). (H8) lib/cloudflare.deployPages already owned the chokepoint (cred-store token via getWranglerToken, env injection, audit logging, stdout URL parsing) but scripts/deploy-watch.sh:56 still shelled out to wrangler directly. Added scripts/cf-deploy-pages.js — a 60-line Node wrapper mirroring the keysvc-encrypt.js pattern: requires lib/cloudflare.deployPages, takes <site-dir> <project> [branch] argv, prints URL on success, exits 2 on chokepoint failure (commit 6257776). Switched deploy-watch.sh to invoke that wrapper (commit 893043c). No raw wrangler calls remain in the repo outside lib/cloudflare.js. (C1) Discovered during investigation that apps/_shared/worker/cf-access-jwt.js already exists and uses jose.jwtVerify against createRemoteJWKSet for koda-systems; all three Tier 2 workers (det22, usermgmt, koda-dashboard) import verifyAccessJwt from it. Audit doc was stale on this point — corrected. Doc updates: external-integration-audit.md C1/H1/H3/H8 marked resolved with diagnoses; bypass table at §4.2 struck through three rows; §3.1 Cloudflare row updated to show lib/cloudflare.js as chokepoint; §3.4 scheduler narrative updated; Week-1 punch list items 5/6/8/9 struck through; surprise §7 'validateCfJwt' bullet struck through; -25 +16 lines (commit 226fcc5). Three restarts (steps 2, 4 — step 3 hot-reloaded). Plan executed in auto mode. |
| 1 May 20266:10 pm |
deploy-watch |
1.1.2current |
Route Pages deploy through the cloudflare-deploy chokepoint via scripts/cf-deploy-pages.js Node wrapper. Replaced raw `wrangler pages deploy "$SITE_DIR" --project-name "$PROJECT" --branch main` with `node "$KODA_DIR/scripts/cf-deploy-pages.js" "$SITE_DIR" "$PROJECT" main`. CF_TOKEN export retained as fallback — lib/cloudflare.getWranglerToken reads from the encrypted credentials table (domain='cloudflare-wrangler') first and only consults env if PostgREST is unreachable. Net effect: deploy-watch now inherits the chokepoint's audit logging, env injection (CI=1), maxBuffer=16MB, and structured error handling. EIP H8 closure. Commit 893043c. |
| 1 May 20265:30 pm |
koda-core |
2.86.0 |
EIP audit finding H6 resolved — deleted the ENC: env-var encryption scheme. Investigation found two layers of brokenness: (a) the server.js:12-35 boot IIFE inline-decrypted ENC:-prefixed env vars by reading .secrets/credential_key directly, but that file was deleted in 2.83.0 (keysvc cutover) so any ENC: value would have hit ENOENT; (b) /config set never actually wrote ENC: prefixes anyway — it called encryptCredentialAsync() without format:'env', defaulting to credential format which has no prefix. The decrypt path was unreachable in practice. Plus, the only two SENSITIVE_KEYS (SIGNAL_CLI_REST_API, POSTGREST_URL) were localhost URLs that don't need encryption. Real secrets live in the credentials table via /cred, encrypted through keysvc. Changes: lib/commands/config.js — removed encryptCredentialAsync import, SENSITIVE_KEYS array, the conditional encryption at storeValue, and the redaction in /config get + /config list (commit 6283b3b, hot-reloaded). server.js — deleted the decryptEnvSecrets() IIFE (-26 lines, commit d28a5cc, restart). Doc updates: external-integration-audit.md marked H6 resolved + struck-through the bypass row + struck-through the Week-1 #7 recommendation; install-notes ENC: auto-decrypt claim removed (commit 8d4689e). Net effect: one fewer chokepoint bypass, /config behaviour now honest (cleartext-only operational tunables; secrets go to /cred). One restart, no regressions. |
| 1 May 20265:00 pm |
koda-core |
2.85.0 |
EIP Q1 follow-up #3 task #4b — removed sync crypto API and lib/keysvc-bridge-cli.js. After task #3 zero'd the sync-bridge call rate on hot paths, a re-audit found 3 missed sync callers (lib/api/credentials.js create-flow, scripts/scheduled-jobs/watchdog.js getResendApiKey, scripts/get-cred.js) plus 2 already-async scripts (scripts/wandi-control.js, scripts/web-control.js) and tests/run_tests.js. Path A: migrated each to decryptCredentialAsync (one file per commit, 14555bd / 4ab975c / e07a6e7 / 97308fc / b1199cc / 7df7942), introduced scripts/keysvc-encrypt.js as the standalone async helper for shell-script consumers (commit dfc418c), updated scripts/cred-set.sh to call it (commit fd06a40), then deleted the sync surface from lib/crypto.js (commit c3ce24c — removed encryptCredential/decryptCredential/decryptEnvValue/getCredentialKey/_bridgeCall/_encryptWithFileKey/_decryptWithFileKey/_warnFallback/getCredKeyPath/cachedCredentialKey/BRIDGE_CLI/NODE_BIN, kept encryptCredentialAsync/decryptCredentialAsync/maskCredential/KEYSVC_REQUIRED/KEYSVC_DISABLED) and removed the now-orphan lib/keysvc-bridge-cli.js (commit 7c77b5e — 78 lines). KEYSVC_DISABLED behaviour changed: previously bypassed keysvc and used file-key fallback; now throws EKEYSVC_DISABLED since file-key fallback was deleted in EIP #19 phase 3 (2.83.0). Verification: 61/61 tests pass, /health ok, get-cred 40-char plaintext, wandi-control returns Active+$43.60+~13days via direct method, cred-set roundtrip MATCH via new keysvc-encrypt.js path. Two restarts (steps 2, 11). |
| 1 May 20264:00 pm |
koda-core |
2.84.0 |
EIP Q1 follow-up #3 — sync→async crypto migration. Fixed scripts/wandi-control.js, scripts/web-control.js, tests/run_tests.js (broken since keysvc cutover 2.83.0 — local AES blocks read deleted .secrets/credential_key); replaced with require('lib/crypto') so the sync API routes through keysvc. Migrated 6 lib/* hot-path callers (auth-headers, calendar, cloudflare _resolveApiToken, intents _getServiceToken cache, api/push initializeVapid, api/credentials with Promise.all over rows.map) and 2 commands (cred 4 sites, config 1 site) to decryptCredentialAsync. server.js services injection now exposes encryptCredentialAsync/decryptCredentialAsync; removed dead getCredentialKey() warn-probe at server.js:1666 (predated keysvc, logged a benign ENOENT warn on every restart since 2.83.0). Measured 24h sync-bridge call rate at 66/day pre-task-#3 and 0/day post-task-#3 (audit_log requestor='koda-core-sync-bridge'); decided to keep lib/keysvc-bridge-cli.js and the sync API in lib/crypto.js as a defensive fallback for CLI scripts (scripts/get-cred.js, scripts/cred-set.sh, scripts/wandi-control.js, scripts/web-control.js, tests/run_tests.js) — the bridge adds ~30-50ms per call but is no longer hit on hot paths. Three restarts (tasks #2, #3) with no regressions; rollback path via git revert documented in each task. Coverage: ~25 call sites across 13 files. Commits 784117a/5f56769/aee3da3 (scripts), 2df794d (lib/*), 38b0037 (commands+server.js). |
| 1 May 20263:00 pm |
koda-core |
2.83.0 |
EIP Q1 carryover trio landed in one minor — #281 daily Tier 2 permission sync cron, #20b koda-dashboard CF Access service-token migration, #19 phase 2/3 keysvc cutover. (1) **#281 daily reconcile cron**: inserted scheduled_tasks row id=7 'App Reconciliation Daily' (cron `0 3 * * *`, command_type=script, command=`scripts/scheduled-jobs/reconcile-apps.js`) — fires 03:00 Australia/Melbourne nightly to detect and sync Postgres↔D1 user/role drift across all Tier 2 apps via the existing /api/sync/permissions chokepoint. Moved scripts/reconcile-apps.js → scripts/scheduled-jobs/ to satisfy the scheduler's realpath-resolved allowlist; updated relative require paths and header doc. First diagnostic run uncovered two issues fixed in-flight: (a) usermgmt had no workspaces/workspace_surfaces row at all (caused 'No workspace linked' error in sync) — inserted workspaces row name='usermgmt', workspace_surfaces row mapping app→workspace, and owner role on the usermgmt workspace; re-ran reconcile, usermgmt now syncs cleanly (1 user). (b) det22 D1=5 vs Postgres=1 user mismatch — root-caused to handleSyncPermissions in apps/det22/src/worker/admin.js doing upsert-only with no stale-row pruning; flagged as a separate follow-up plan (worker change + deploy, out of scope here). Cron activates on next Koda Core boot since the in-process scheduler reads from DB at startup. (2) **#20b koda-dashboard CF Access**: generated dedicated service token via Cloudflare API POST /accounts/{id}/access/service_tokens (name='koda-dashboard', client_id ending `.access`, expires 2027-04-30), stored via cred-set.sh as cf-access-koda-dashboard with AES-256-GCM-encrypted `id:secret` payload (verified decrypt round-trip). Worker-side: added authenticateAdmin() to apps/koda-dashboard/src/worker/middleware.js mirroring det22's pattern (CF Access JWT primary with EXPECTED_SERVICE_TOKEN_CN === jwt.common_name match, ADMIN_API_KEY bearer fallback for break-glass); apps/koda-dashboard/src/worker/admin.js now uses it for /admin/health (auth-decides-detail-level — unauth responds shape-only, auth includes recent_audit_count + service_token_authenticated flag) and ALL other /admin/* endpoints. Added EXPECTED_SERVICE_TOKEN_CN = `<client_id>` to wrangler.toml [vars]; deployed via lib/cloudflare.js.deployWorker. **Caught a sleeper bug**: koda-dashboard had been deployed without the CF_ACCESS_AUD secret entirely — apps/_shared/worker/cf-access-jwt.js throws if env.CF_ACCESS_AUD unset, so first request returned Worker error 1101 (worker_threw_exception). Fixed via lib/cloudflare.js.putSecret CF_ACCESS_AUD = `39bd00b22989f16a5a00e50315b6ce1788a7acfadb612cc7e16a609ed4931ab6` (wildcard *.koda.systems aud tag) and updated the *.koda.systems CF Access app 'Service Token Access' policy to include the new token_id. Final verification: dash.koda.systems/admin/health returns HTTP 200 with `recent_audit_count:3` proving the service-token auth path resolves correctly. lib/sync.js was already calling getCfAccessHeadersForApp('koda-dashboard') so no Koda Core code change was required for the dashboard side. Commits cf998e5 (worker auth), 1e65c1d (wrangler.toml + AUD secret + policy). (3) **#19 phase 2/3 keysvc cutover**: previously all credential AES-256-GCM encrypt/decrypt in Koda Core read the key from `.secrets/credential_key` (perm 0600, koda-readable) — this phase moves the key into the kodasvc trust domain at /var/lib/koda-keysvc/key (perm 0400, kodasvc-only) and routes every encrypt/decrypt through the keysvc sidecar over a Unix socket, then deletes the on-disk key from the koda user. New `lib/keysvc-client.js` — JSON-line socket client (NOT HTTP — single-line JSON over net.Socket, \n-delimited; corrected initial assumption); 7-scenario self-test passed including the critical legacy-compat check that ciphertext produced by lib/crypto.js.encryptCredential decrypts correctly via keysvc.decrypt, proving keyfile byte-identity. Typed error codes (ETIMEDOUT, ENOENT, EKEYSVC_ERROR, EKEYSVC_PROTOCOL). Refactored `lib/crypto.js`: sync encryptCredential/decryptCredential now route through keysvc via new `lib/keysvc-bridge-cli.js` (one-shot spawnSync subprocess, ~30-50ms overhead per call — preserves the sync API contract used by ~15 callers in Koda Core without forcing every call site to async); new async exports encryptCredentialAsync/decryptCredentialAsync skip the spawn for new code paths. KEYSVC_REQUIRED env gate: unset = warn mode (try keysvc, fall back to file-key on error with one-shot warn log per direction); KEYSVC_REQUIRED=true = required mode (any keysvc error throws, file-key fallback unreachable). Phase 2 activation: appended KEYSVC_REQUIRED=true to .env directly (not via /config since CONFIGURABLE_KEYS allowlist excludes platform infra knobs); restarted Koda Core via launchctl kickstart -kp gui/$UID/com.koda.core; post-restart audit log shows 28 successful decrypts + 3 successful encrypts from requestor 'koda-core-sync-bridge' with ZERO failures, including boot-time decryption of CLOUDFLARE_API_TOKEN/CF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRET — would have thrown if any path had bypassed keysvc. Phase 3 keyfile removal: confirmed shasum identity between /var/lib/koda-keysvc/key and .secrets/credential_key (cdbadb4f54ca2bee64b73e21e25043715215fd012f667e27cedc2788c4497fea on both); soft-removed via mv → tmp/credential_key.removed-step9-1777601221.bak (rollback-friendly, hard-deletable in a few days); fresh node process spawned with KEYSVC_REQUIRED=true loaded from .env via dotenv decrypted all 15 stored credentials → 15/15 ok 0 fail, proving the file-key fallback path is now dead (file gone + KEYSVC_REQUIRED throws on attempt). Running Koda Core process unaffected (cached buffer in memory irrelevant since KEYSVC_REQUIRED=true makes fallback unreachable); on next restart the server.js:1666 getCredentialKey() probe will throw + log a benign warn line — non-fatal because wrapped in try/catch. Rollback path documented: restore tmp/credential_key.removed-*.bak → /Users/koda/koda/.secrets/credential_key + remove KEYSVC_REQUIRED line from .env + kickstart restart. Commits 2d5c569 (keysvc-client), eff105f (crypto refactor + bridge-cli). All three EIP carryovers complete; Q1 #19/#20b/#281 closed. |
| 1 May 20261:20 pm |
det22 |
1.10.2 |
EIP Q1 follow-up #1 — fix stale-row drift in handleSyncPermissions (apps/det22/src/worker/admin.js). Daily reconcile cron (#281) had been reporting det22 D1=5 vs Postgres=1 user mismatch because the sync handler was upsert-only with no DELETE pass. Added stale-row pruning after the upsert loop with three guards: (1) skip entirely if incoming users.length === 0 (defends against accidental wipe via a malformed payload), (2) only delete is_proxy=0 rows (proxy users are admin-created inside the app, never in the Postgres canonical list), (3) emit audit_log entry per deletion BEFORE the DELETE so user_id/email/reason are captured (action='sync.user_deleted', actor='koda-core', channel='sync'). FK behaviour on det22: rsvps.user_id and availability.user_id ON DELETE CASCADE so those auto-clean; activities.created_by has no cascade but the column is nullable so dangling references don't error in D1 — leaving them is acceptable since activities don't strictly need a valid creator post-creation. Response payload now includes a `deleted` field. Worker deployed via lib/cloudflare.js.deployWorker (audit row 3508, 8.4s). Verification: post-deploy syncPermissionsToApp('det22') returned ok+synced=1, /admin/health user_count dropped 5→1, recent_audit_count=4 (the 4 stale rows pruned), alerts list empty. Drift cleared. Commit c5af0c5. |
| 1 May 20261:20 pm |
usermgmt |
1.8.2 |
EIP Q1 follow-up #1 — same DELETE-not-in pass added to apps/usermgmt/src/worker/admin.js handleSyncPermissions (cross-fix to keep both Tier 2 worker handlers consistent). usermgmt's app_users had no stale rows at the time of fix (recent_audit_count=0 after sync) but the bug was structurally identical — upsert-only with no pruning — so this is preventative. usermgmt's app_users has no inbound FKs (managed_users and role_assignments use a separate managed_user_id PK), so plain DELETE is safe with no cascade considerations. Same three guards (empty-list skip, is_proxy=0 filter, audit_log per deletion) and same `deleted` field in response payload. Worker deployed via lib/cloudflare.js.deployWorker (audit row 3509, 9.5s). Post-deploy verification: sync ok, user_count=1, alerts empty. Commit 60b7b23. |
| 1 May 20261:00 pm |
koda-core |
2.82.0 |
EIP Q1 #21 closeout — bundle gate hard-fail flip + /bundles reload alias. Two related changes that finalise the retrofit arc (P1-P4 shipped 13/13 bundles in 2.78.0-2.81.0; this is the gate-tightening follow-up). (1) `lib/intents.js.checkBundleGate` Case C flipped from soft-fail to hard-fail. Before: bundle absent → if manifest.bundle_required=true refuse, else log warn + audit `intent.bundle_missing` + dispatch (silent allow — transitional state during the retrofit). After: bundle absent → unconditional refusal, log warn `dispatch gate refused (no bundle for app)`, audit `intent.refused` with reason `no_bundle_registered`, DM owner via _shouldDmBundleRefusal/_dmOwnerBundleRefusal pattern (DM message includes the fix path: `Add bundles/X.yaml then /bundles reload.`), return {allowed:false, refused:true, reason}. The bundle_required manifest opt-in branch dropped entirely — irrelevant now that all Tier 2 apps must declare a bundle. JSDoc Cases comment block updated to reflect the simplified two-branch logic (Case A allow, Case B refuse-action-missing, Case C refuse-bundle-missing — all three with consistent refusal semantics on miss, single allow branch on hit). (2) `lib/commands/bundles.js` adds `/bundles reload` subcommand as an alias for the existing `validate`. Both call `registry.refresh()` → `loadAllBundles()` → bundles.clear() + re-scan disk; identical behaviour, just a more discoverable name when an owner is debugging a Case-C refusal and naturally searches for 'reload' rather than 'validate'. Shared code path via `if (sub === 'validate' || sub === 'reload')` block; reply title varies ('Bundle registry validate' vs 'Bundle registry reload') based on which alias was invoked. HELP_TEXT and top-of-file JSDoc subcommand list both updated. Hot-reloaded — no restart required to activate the alias itself; restart for this version is solely for the lib/intents.js Case C flip. **Safety check confirmed before flip**: of the 4 koda_apps DB rows (console, koda-dashboard, usermgmt, det22), only usermgmt and det22 produce intents — both have bundles. console is handled by the tier-check before the gate (returns 'console tier not implemented'); koda-dashboard has empty manifest with no intents declared so never reaches the gate. No current Tier 2 app refused by the flip. **Repeatable fix path** for any future missed integration: owner sees DM → identify chokepoint module → create bundles/X.yaml from SCHEMA.md template → validate via parser smoke test → commit via self-modify.sh (no restart for YAML) → /bundles reload. **Anti-silent-failure design preserved**: gate explicitly returns {allowed:true} on registry lookup throw (line 105 catch — fail-open on internal errors), so a registry hiccup never silently denies. Refusals surface 5 ways: chat reply to requestor, owner Signal DM (30-min app.action dedupe, resets on restart), audit_log row, core.log warn line, anomaly watchdog over audit_log volume. Commits TBD-versions / TBD-bundles-reload / TBD-intents. |
| 1 May 202612:00 pm |
koda-core |
2.81.0 |
EIP Q1 #21 P4 retrofit + activation — final 2 bundles shipped (web-control, pdf-generation), then single restart activates all 8 P2+P3+P4 inactive bundles together. Registry coverage 11/13 → 13/13 (FULL — every external integration target now has a registered bundle), 54 → 62 declared actions. (1) `bundles/web-control.yaml` — 3 actions (wandi_status read-only, wandi_pause destructive, wandi_unpause destructive); integrations: [lib/commands/wandi.js, scripts/wandi-control.js]; chokepoint_fn=runScript (internal helper in wandi.js — bundle documents the script-wrapper indirection per ADR-019: single-vendor script wrappers stay as slash commands rather than Tier 2 koda_apps until 5+ accumulate); documents the Wandi/Launtel dispatch (slash command → execFile node scripts/wandi-control.js with 60s timeout → dual-path: direct HTTP via cached session cookies primary, Playwright browser automation fallback when Launtel UI changes), exit code semantics (0=direct ok, 1=fallback ok handler attaches _fallback:true, 2=both failed throws with stderr), site config layout (data/web-control/wandi-internet/config.json + session-state.json + http-cookies.json with 30-min TTL), $5.50 baseline reconnection fee for unpause (flag if config.json drifts), the 'push through unpause fully without pausing for confirmation' protocol per memory (owner has already opted in by typing /wandi unpause; mid-flow confirms are friction); anti-patterns: don't axios residential.launtel.net.au directly (script owns CSRF/cookies/UA/dual-path/exit-codes), don't run Playwright manually for status (~3s direct vs ~20s Playwright), don't dispatch unpause from chat without explicit owner confirm (Claude-initiated, $5.50 non-recoverable), don't loop pause/unpause in cron ($132/day if hourly), don't treat exit 1 as failure (operation completed), don't hardcode wandi-specific values, don't extend bundle to other ISPs without script refactor. (2) `bundles/pdf-generation.yaml` — 5 actions (generate_document dispatcher, generate_md, generate_pdf, generate_pptx, generate_docx — format-specific helpers exposed for callers that already know the format); integrations: [lib/document-generator.js, lib/pdf-templates/index.ts]; chokepoint_fn maps to generateDocument/generateMD/generatePDF/generatePPTX/generateDOCX exports; NOT destructive (generation produces new files at new paths, never overwrites — duplicate filenames get numeric suffix); documents the React-PDF Mockup D v7 template library lock-in per memory (always import from lib/pdf-templates barrel: palette/styles/docTypeConfig/pillVariants/statusPillConfig + KodaPage/GradientHeader/SimpleFooter/Callout/Pill/StatusPill/Bullet/DataTable/DiagramContainer; never re-declare fonts/colours/styles/components inline; redundant scripts/pdf-template.tsx was deleted for this reason), 4 canonical doc types (ANALYSIS/RESEARCH/TRANSCRIPT/REPORT) with example-image manifest workflow per memory (check hash → regenerate if stale → present to user → generate), Inter+Source Serif font discipline (never switch mid-doc), the @react-pdf/render Canvas painter patch (linearGradient/radialGradient must return gradient object — patch-package'd, required by GradientHeader, removal breaks header silently), two PDF paths (generatePDFReactPDF primary, generatePDFPlaywright fallback only when React-PDF refuses), auto-share via lib/file-share.js with default site/shared/<guid>/index.html (NEVER publish to site/ root per memory — only on explicit owner request to a named path), 30-day CF Pages expiry, deploy-watch trigger; anti-patterns: don't re-declare template primitives inline, don't bypass file-share for sharing, don't publish to site/ root, don't switch fonts mid-doc, don't use Playwright as primary path, don't skip example-image manifest check, don't iterate PDF mockups via koda.systems URL (use Signal attachments per memory — Safari rendering can drift), don't call pptxgenjs/docx/@react-pdf/renderer directly, don't remove the Canvas patch. (3) Single restart via `launchctl kickstart -k gui/$UID/com.koda.bridge` activates all 8 inactive bundles from P2+P3+P4 (tasks, scheduler, transcription, cloudflare-deploy, vscode-bridge, web-push, web-control, pdf-generation) — boot-time bundle registry preload reads bundles/*.yaml from disk, restart is the activation trigger. Post-restart: registry coverage 13/13 (FULL), 62 declared actions across 13 bundles. **Q1 #21 retrofit arc COMPLETE** across 4 phases (P1: det22+usermgmt+signal+whatsapp / P2: tasks+scheduler+transcription / P3: cloudflare-deploy+vscode-bridge+web-push / P4: web-control+pdf-generation). Every external integration target on Koda Core now routes through a registered chokepoint with EIP three-runtime defence (matcher preload at write-time, self-check skill at draft-time, dispatch gate at execution-time). Separate follow-up plan will flip the bundle gate from soft-fail (Case C log+allow) to hard-fail (refuse unregistered actions). Bundles parse cleanly through lib/bundles/parser.js; commits fd2e796 (web-control), 5f960ed (pdf-generation). |
| 1 May 202611:00 am |
koda-core |
2.80.0 |
EIP Q1 #21 P3 retrofit — 3 bundles shipped (cloudflare-deploy, vscode-bridge, web-push). Registry coverage 8/13 → 11/13 bundles, 45 → 54 declared actions. (1) `bundles/cloudflare-deploy.yaml` — 4 actions (deploy_pages, deploy_worker, put_secret, apply_d1_migrations), ALL destructive (Pages/Worker overwrite live deployment, put_secret is irreversible without external backup, D1 migrations are forward-only DDL with no `wrangler migrations rollback`); integrations: [lib/cloudflare.js]; chokepoint_fn maps to deployPages/deployWorker/putSecret/applyD1Migrations exports; documents the credential-store flow (PostgREST credentials table domain='cloudflare-wrangler' decrypted via lib/crypto.js, env fallback only as a hot-path optimisation since server.js loads it at startup), wrangler binary resolution sequence (WRANGLER_BIN env → /opt/homebrew/bin/wrangler → ~/.nvm/versions/node/v24.14.0/bin/wrangler → /usr/local/bin/wrangler → `which wrangler`), audit_log namespace (cloudflare.pages.deploy / .worker.deploy / .secret.put / .d1.migrate) emitted BEFORE throw on failure (missing audit row indicates chokepoint crash, not wrangler failure), CI=1 env flag to suppress wrangler interactive prompts, 16MB maxBuffer for large deploys, stdin-piping for `secret put` (NEVER argv — process listings leak), the `deployer` audit-actor convention ('deploy-watch' for fswatch pipeline, 'app-provisioner' for Tier 2 provisioning, 'manual:klaus' for owner-initiated), no-auto-retry policy (deploy-watch retries via next file change; manual deploys are owner-investigated); anti-patterns: don't shell out to wrangler directly, don't read CLOUDFLARE_API_TOKEN from env for new code, don't pass secret value on argv, don't auto-retry deploy failures, don't expect D1 migration rollback, don't skip audit_log writes, don't bypass for cron without bypassToken. (2) `bundles/vscode-bridge.yaml` — 4 actions (invoke_vscode, check_vscode_health, fetch_vscode_models, set_vscode_model), read-side no destructive_actions; integrations: [lib/vscode.js]; documents the Copilot routing rationale (avoiding re-implementing Copilot auth in Koda Core by running a VS Code extension on kodahost that proxies OpenAI-style requests to Copilot's authenticated session), 900s default timeout via VSCODE_BRIDGE_TIMEOUT_MS, /health vs /ready distinction (the chokepoint's checkVSCodeHealth uses /health + data.models.length>0 assertion to detect 'bridge up but Copilot dead' without hanging, since /ready hangs when Copilot is unauth'd), Copilot auth recovery sequence (SOCKS proxy on Mac Mini 192.168.100.10 → kodahost network reconfig → re-auth Copilot in VS Code), AbortController/AbortSignal.timeout enforcement, sessionId always null (bridge does not surface Copilot-side resume), state.vsCodeSelectedModel pin behaviour ('auto' clears, any other ID pins); anti-patterns: don't fetch the bridge directly without timeout/abort handling, don't curl /ready with short timeout (hangs when Copilot unauth'd), don't lower 900s default for unknown-length prompts, don't cache fetch_vscode_models across sessions, don't treat checkVSCodeHealth=false as 'bridge offline' (usually means Copilot died not bridge). (3) `bundles/web-push.yaml` — 1 action (notify_user, chokepoint_fn=pushNotifyUser), send-only not destructive; integrations: [lib/api/push.js]; documents the deliberate single-action surface (initializeVapid is boot-only; subscribe/unsubscribe/status/test are HTTP endpoints invoked by PWA frontend not server-callable), three-layer messaging context (L1 SSE foreground / L2 long-poll fallback / L3 web push background — fire only when L1+L2 absent), VAPID init (public key in .env, private key in credentials table domain='vapid-keys' decrypted via lib/crypto.js with legacy-unencrypted warning), per-subscription send loop, auto-cleanup on 410/404 (keeps push_subscriptions table from accumulating zombie rows from uninstalled PWAs), last_used_at touch on success, payload shape ({title,body,tag?,url?}) and lock-screen visibility caveat (use generic title for sensitive content, put detail behind click-through url), tag field for collapse-key dedup, lazy VAPID init pattern, never-throws contract (returns 0 on any failure so messaging layer can fall back to other channels); anti-patterns: don't call webpush.sendNotification directly (loses lazy init/enumeration/cleanup/last_used_at touch), don't iterate push_subscriptions yourself for 'more control' (cleanup is safety-critical for table-growth bound), don't put sensitive content in payload.body (lock-screen visible by default), don't push when SSE/long-poll active (gate on presence first), don't loop notify_user for batches without tag (use tag for collapse-key dedup), don't initializeVapid outside the chokepoint (race vs in-flight sends), don't treat return=0 as failure (means fall back to another channel). All 3 bundles parse cleanly through lib/bundles/parser.js (loadBundleFile validates name/version/description/integrations/permissions/destructive_actions cross-check/instructions/actions); commits 6dd9511/1d62401/33a9f8a. P3 + P2's 6 inactive bundles will activate together at next Koda Core restart (deferred to end of P4 to amortise restart cost). Two retrofits remain (P4): web-control, pdf-generation. |
| 1 May 202610:00 am |
koda-core |
2.79.0 |
EIP Q1 #21 P2 retrofit — 3 bundles shipped (tasks, scheduler, transcription). Registry coverage 5/13 → 8/13 bundles, 29 → 45 declared actions. (1) `bundles/tasks.yaml` — 7 actions (list_tasks, create_task, update_task, mark_done, delete_task, get_task_info, work_tasks), 2 destructive (delete_task hard DELETE FROM, mark_done flips status with no history table); integrations: [lib/postgrest.js, lib/commands/tasks.js]; chokepoint_fn maps to pgGet/pgPost/pgPatch/pgDelete on the `tasks` table; documents the user-facing `tasks` table schema (id/title/status/priority/scope/scope_id/labels/due_date), the global/app/group scope hierarchy + auto-resolution heuristics in resolveContextScope(), the `lib/tasks.js` background-runner naming collision (intentionally separate surface), the work_tasks side-effects (writes per-task instruction files + queue entry, flips status to in_progress); anti-patterns: don't axios PostgREST directly, don't bulk-delete in a loop (use executeBatch with single confirm), don't skip updated_at=now() on patches, don't generate instruction files for already-done tasks. (2) `bundles/scheduler.yaml` — 8 actions (list_scheduled_tasks, create_scheduled_task, update_scheduled_task, enable_scheduled_task, disable_scheduled_task, delete_scheduled_task, run_scheduled_task_now, list_runs), 1 destructive (delete_scheduled_task — task definition gone, run history kept via no-cascade FK); integrations: [lib/scheduler.js, lib/postgrest.js]; documents the singleton boot pattern (loads enabled rows at boot, in-memory Map of taskId→cronJob, reload() needed after DB writes), the three command_types (slash dispatched via registry as synthetic owner context; script shell-out with realpath-resolved scripts/scheduled-jobs/ allowlist + shell-metachar refusal; prompt direct spawnSync claude — known bypass of lib/claude.js tracked in audit doc §4.2); the KODA_SESSION_ID=<id> stdout contract for resumability, expects_session=true guardrail, scheduler.task.refused audit + owner DM on allowlist violation, Australia/Melbourne tz; relevant scheduled_tasks columns + scheduled_task_runs history table; anti-patterns: don't pgPost without reload(), don't bypass allowlist, no shell metacharacters in command, don't add prompt-type tasks for security-sensitive flows until invokeClaudeCode migration done, use disable_scheduled_task to pause (preserves row + history), don't manually edit scheduled_task_runs, don't call _execute* methods directly. (3) `bundles/transcription.yaml` — 1 action (transcribe_voice_note), read-only no destructive_actions; integrations: [lib/voice-transcribe.js]; chokepoint_fn: transcribeVoiceNote; documents the deliberate split from the koda-transcribe MCP (this bundle = chat-latency ephemeral small.en for inbound voice notes; MCP = persistent searchable medium model for podcasts/videos), the two-step pipeline (ffmpeg /opt/homebrew/bin/ffmpeg → 16kHz mono WAV with 15s timeout → whisper-cli /opt/homebrew/bin/whisper-cli with 30s timeout), the model bias prompt 'Koda. Wandi. DET22. koda.systems. Klaus.' (whisper otherwise mishears Koda→Coder, Klaus→class, Wandi→Wendy), empty-transcript guard surfacing 'no speech detected' to caller; anti-patterns: don't shell out to whisper-cli/ffmpeg directly (chokepoint owns bin paths, timeout, tmpdir cleanup, bias, empty-transcript guard), don't use for long-form (use the koda-transcribe MCP), don't cache transcripts here, don't pass relative paths, don't switch to medium model for chat latency, don't swallow empty-transcript errors silently. All three bundles parse cleanly through lib/bundles/parser.js (loadBundleFile validates name/version/description/integrations/permissions/destructive_actions cross-check/instructions/actions); commits b9e7be5/62ca72c/e310dc6. Five retrofits remain (P3-P4): cloudflare-deploy, vscode-bridge, web-push, web-control, pdf-generation. |
| 1 May 202612:00 am |
koda-core |
2.78.0 |
EIP Q1 #21 P1 retrofit — 4 bundles shipped (det22, usermgmt, signal, whatsapp). Registry coverage 1 → 5 bundles, 5 → 29 declared actions. (1) `bundles/det22.yaml` — 9 actions, 2 destructive (delete_activity, clear_availability), all wired to `lib/intents.js.executeIntent` with CF Access dual-layer auth (cf-access-det22 cred + X-Koda-Service-Token HMAC) documented; substantial instructions block covers the 5-step dispatch path, 4-tier RBAC, identity-action security verified params (rsvp.on_behalf_of, set/query/clear_availability.user_id), anti-patterns (no direct fetch to det22.koda.systems, no name-based references in verified params, no D1-direct writes). (2) `bundles/usermgmt.yaml` — 4 actions, 2 destructive (update_user, assign_role); assign_role's cascading_role gate documented (requestor's effective role must outrank params.role per identity-action-security.md Layer 3); 4-role RBAC scoped per-app; anti-pattern call-out that lib/usermgmt-client.js is the read-side sync mirror, not the mutation path. (3) `bundles/signal.yaml` — 3 actions on lib/channels/signal.js (send_message, send_to_group, send_chunked); NOT destructive; signal-cli REST API auth model documented (localhost-bound sidecar, no per-call auth); markdown→styled three-phase converter documented; signal-notify.sh:77 curl bypass flagged as the one justified bypass (restart scenarios where the lib isn't loaded). (4) `bundles/whatsapp.yaml` — 8 actions on lib/channels/whatsapp.js (send_message, send_file, send_chunked, send_seen, start_session, get_session_status, get_media_file, fetch_inbound_media); NOT destructive; WAHA dual-layer auth (localhost network + WAHA_API_KEY) documented; server.js:1555-1556 axios.get(.../api/default/groups/...) flagged as a known bypass requiring a future getGroupInfo chokepoint (P3 retrofit); waha-media path-traversal discipline documented; messaging.read_media gate for media-fetch actions. Validated end-to-end: registry parses all 5 bundles cleanly, getActionDef resolves the representative action per bundle, checkBundleGate hits Case A (allowed=true) on a synthetic det22.create_activity intent. P1 audit doc scoreboard refresh: Tier 2 Workers (det22+usermgmt) Reg. AMBER → GREEN (now 4/5); Signal Reg. RED → GREEN (now 2/5); WhatsApp Reg. RED → GREEN (now 2/5). Eight retrofits remain (P2-P4): tasks, scheduler, transcription, cloudflare-deploy, vscode-bridge, web-push, web-control, pdf-generation. |
| 30 Apr 202610:30 pm |
koda-core |
2.77.1 |
EIP audit hygiene + anomaly alert UX. (AMBER-5) `/bundles show` validator: dropped the `i` flag from the regex so `MixedCase` names get a clear 'Invalid bundle name. Bundle names must be lowercase-kebab' error instead of the misleading 'Bundle not found. Try /bundles list' that the case-sensitive registry lookup produced after the case-insensitive validator passed (`lib/commands/bundles.js`). (AMBER-6) `/help` Bundle Router (EIP) section now lists all three subcommands inline (`/bundles list /bundles show <name> /bundles validate`) instead of the bare `/bundles` it carried before — matches the multi-subcommand pattern used elsewhere in /help (`lib/commands/help.js`). Sister-changes (no koda-core source touched): (1) `docs/anomaly-rules.json` — destructive-action-off-hours rule excludes `session_reset` from the destructive-action match (regular UX action, was firing false positives nightly when the owner reset a console session in off-hours); (2) `scripts/scheduled-jobs/anomaly-watchdog.js` — when a rule fires, the watchdog now does a follow-up GET for up to 5 sample rows and appends `• <action> @ HH:MM <app> <channel>` lines to the alert DM, plus `…and N more` if total exceeds sample size, so the owner sees what triggered the rule instead of a bare count; (3) `docs/external-integration-audit.md` refresh — header version 2.68.0 → 2.77.0, §1 scoreboard split Tier 2 row (det22+usermgmt → 3/5 with CF Access service tokens, dashboard → 2/5), added Google Calendar row (5/5), §4.3 narrative refreshed to note calendar is now LIVE end-to-end and pre-flight matcher wiring is shipped, §6 #11b strikethrough since /db proxy was killed in 2.71.0. |
| 30 Apr 202610:00 pm |
koda-core |
2.77.0 |
EIP Q1 #21 pre-flight — Runtime 1 (bundle matcher) is now live across all five `buildSystemPrompt` call sites: `server.js` handleMention path, `lib/api/chat.js` /api/chat/send, `lib/api/messages.js` POST /api/messages, `lib/commands/modify.js` (both /modify <instruction> plan branch and /modify confirm branch), and `lib/commands/work.js` queue task executor. Each site invokes `matchBundles({ userText, bundles: registry.getAllBundles() })`, resolves matched bundle objects via `getBundle(name)`, audit-logs `bundle.matched` (with `match_scores` detail JSON, channel-aware), and passes the resolved array as the final `bundleMatches` argument to `buildSystemPrompt` so matched bundle instructions land in the system prompt. (RED-2) Bundle registry now preloaded at boot in `server.js` next to `loadApps()` — fail-loud on error so an unloaded registry never goes silently undetected. Closes the lazy-load race that previously dropped first-100ms-after-restart intents into Case C. (AMBER-1) `lib/api/messages.js` `buildSystemPrompt` call had wrong signature (`(groupId, req.user)` — the user object was rendering as `groupInstructions` text); now correctly fetches global+group instructions, user permissions, and memories like `lib/api/chat.js` does. (AMBER-4) `lib/intents.js` BUNDLE_REFUSAL_DM_TTL_MS now has an explicit comment block documenting the accept-as-is decision: in-process Map resets on restart by design — informative signal that the underlying loop is still happening; volumetric anomalies covered separately by audit_log watchdog. Verified live: `audit_log` shows `bundle.matched` rows from owner messages during the rolling restarts (channel=signal, calendar bundle scoring 2 from `update_event` action keyword); boot log shows `[boot] bundle registry preloaded count=2`. Per-bundle retrofit (det22, usermgmt, signal, whatsapp, etc.) and hard-fail flip deferred to follow-up sessions per `instructions/eip-q1-21-bundle-retrofit.md`. |
| 30 Apr 20267:30 pm |
koda-core |
2.76.0 |
EIP Q1 #17 close-out — bundle router runtimes 2 & 3 + tooling. (D, M7-D) Always-on `skills/bundle-self-check.md` skill auto-injected into every Claude system prompt by `lib/instructions.js` (loaded once at module init); makes the model self-check that proposed external calls go through a bundle's chokepoint, refuse cleanly when no bundle exists, and never bypass the registry for chat-surface requests (cron is the only path with a bypassToken). (E, M7-E) Runtime 3 dispatch gate shipped in `lib/intents.js.checkBundleGate` — invoked by both `executeIntent` and `executeBatch` BEFORE the outbound fetch. Three cases: bundle present + action present → pass; bundle present + action missing → refuse with `intent.refused` audit row + owner DM (suppressed per-`app.action` for 30 min via in-process Map); bundle absent → refuse if manifest opts in via `bundle_required: true`, else pass through with `intent.bundle_missing` audit row (transitional state until #21 retrofit). Audit writes flow into existing `audit_log` table — chose this over a dedicated `bundle_router_log` table so the anomaly watchdog already covers it. Imports `getBundle` / `getActionDef` from the lazy-loaded `lib/bundles/registry`; gate fail-opens with a logged warning if the registry hiccups so a transient registry issue cannot block legitimate traffic. (F, M7-F) `/bundles list|show|validate` read-only inspector at `lib/commands/bundles.js` (owner-only, hot-reloaded). `list` shows `name (vX.Y.Z) — description (N actions)` plus a separate Errors section for any YAMLs that failed to parse. `show <name>` pretty-prints the bundle (integrations, permissions, destructive marker, action table with chokepoint_fn + perm, first 1000 chars of instructions). `validate` re-runs `registry.refresh()` and reports load counts + added/removed since previous load; survives a deliberately-broken YAML cleanly. `/help` updated to surface `/bundles` under a new owner-only Bundle Router section. (G, M7-G) Migration helper `scripts/bundle-from-app.js <app> [--out path] [--force]` — bootstraps a starter `bundles/<app>.yaml` from an existing `apps/<app>/manifest.koda.json`. Maps name/version/description/destructive_actions, builds an `actions{}` map with a `TODO_set_chokepoint_fn` placeholder per intent (manifests don't carry one), flattens unique `intent.permission`s, scaffolds an instructions skeleton with a TODO_chokepoint_module marker, validates the generated YAML through `lib/bundles/parser.js` before writing, refuses to overwrite without `--force`. Dry-run on det22 produces a 9-action starter (11 TODOs); usermgmt produces a 4-action starter (6 TODOs); both parse cleanly. Closes EIP Q1 #17 (DB task #263); audit doc §4.3 RED → AMBER and remediation list #17 marked done with as-built notes (telemetry deviation: audit_log instead of bundle_router_log; bundles/ at top level, not skills/bundles/). Outstanding: #21 retrofits det22 / usermgmt / calendar to declare bundles so Runtime 3 stops falling into legacy pass-through for them; #20b koda-dashboard CF Access service token migration. |
| 30 Apr 20266:00 pm |
det22 |
1.10.1 |
EIP Q1 #20 deploy — live cutover. authenticateAdmin() now checks claims.common_name against env.EXPECTED_SERVICE_TOKEN_CN (CF Access puts the issuing token's *client_id* in common_name, not the human token name as the brief assumed; brief deviation documented). CF_ACCESS_AUD secret added (was missing — verifyAccessJwt was throwing 1101 on every JWT path). Worker deployed to det22.koda.systems and live-verified: matching token → 200 (admin/health), cross-app token (usermgmt) → 401. |
| 30 Apr 20266:00 pm |
usermgmt |
1.8.1 |
EIP Q1 #20 deploy — live cutover. Same middleware fix as det22 1.10.1 (common_name vs client_id). CF_ACCESS_AUD secret added. Deployed to users.koda.systems and live-verified: matching token → 200 (admin/health), cross-app token (det22) → 401. |
| 30 Apr 20265:00 pm |
koda-core |
2.75.0 |
EIP Q1 #20 (Koda Core caller side) — admin calls to Tier 2 apps now use per-app CF Access service tokens (locked decision #8). New `getCfAccessHeadersForApp(appName)` helper in lib/auth-headers.js reads from credentials domain `cf-access-${appName}` (encrypted as `<client-id>:<client-secret>` written by scripts/cred-set.sh). Falls back to the shared `cloudflare-access-service` token when the per-app cred is missing, so the rollout can land in stages — every existing /admin/* call keeps working until each CF Access app is reconfigured to require its specific service token at the edge. Per-app result is cached; new `clearCfAccessHeadersCache(appName)` invalidates it after a token rotation. lib/sync.js wired through at all four /admin/* call sites: `_directSyncPermissionsToApp` → cf-access-${appName}, `checkAppHealth` → cf-access-${appName}, `syncDashboardEntitlements` → cf-access-koda-dashboard, `collectAuditLogs` → cf-access-${appName}. Companion to the worker-side change in det22 1.10.0 + usermgmt 1.8.0 (admin endpoints now accept either CF Access service-token JWT with matching common_name claim, or the legacy ADMIN_API_KEY bearer for back-compat). Phase 1 (CF Zero Trust dashboard token issuance) and Phase 5 (worker `wrangler secret put APP_NAME` + `wrangler deploy`) blocked on owner. Also adds scripts/cred-set.sh helper (companion to get-cred.js) for storing encrypted credentials from the CLI; documented chokepoint bypass for direct PostgREST access (same justification as get-cred.js). |
| 30 Apr 20265:00 pm |
usermgmt |
1.8.0 |
EIP Q1 #20 (worker-side) — usermgmt /admin/* endpoints now route through new authenticateAdmin() in src/worker/middleware.js. Primary auth: CF Access service-token JWT, validated by the shared apps/_shared/worker/cf-access-jwt.js verifier; the resulting claims must carry a common_name claim equal to `koda-core → ${env.APP_NAME}` (i.e. `koda-core → usermgmt`). A token issued for any other app's CF Access application is refused with a 403 — this is the cross-app admin-spoofing block called out in locked decision #8. Legacy `Authorization: Bearer ${env.ADMIN_API_KEY}` is kept as a back-compat fallback so existing Koda Core sync calls continue to work until CF Access is configured to require the service token at the edge for /admin/* (Phase 1 + 5 of the instruction, blocked on owner). Bundle now declares `cf_access: { service_token: cf-access-usermgmt, common_name: koda-core → usermgmt }` for chokepoint discoverability. /api/* user flow is untouched — only /admin/* moved. |
| 30 Apr 20265:00 pm |
det22 |
1.10.0 |
EIP Q1 #20 (worker-side) — det22 /admin/* endpoints now route through new authenticateAdmin() in src/worker/middleware.js. Primary auth: CF Access service-token JWT, validated by the shared apps/_shared/worker/cf-access-jwt.js verifier; the resulting claims must carry a common_name claim equal to `koda-core → ${env.APP_NAME}` (i.e. `koda-core → det22`). A token issued for any other app's CF Access application is refused with a 403 — cross-app admin spoofing blocked. Legacy `Authorization: Bearer ${env.ADMIN_API_KEY}` is kept as a back-compat fallback so existing Koda Core sync calls keep working until CF Access enforces the service token at the edge for /admin/* (Phase 1 + 5 of the instruction, blocked on owner). Bundle now declares `cf_access: { service_token: cf-access-det22, common_name: koda-core → det22 }`. /api/* user flow is untouched. |
| 30 Apr 20264:00 pm |
keysvc |
0.1.0current |
EIP Q1 #19 — koda-keysvc readiness artifacts. New keysvc/ directory ships a tiny single-purpose Node sidecar (stdlib only) that owns the credential encryption key and exposes encrypt / decrypt / ping over a Unix socket (default /var/run/koda-keysvc.sock, perm 0660 owner kodasvc:koda). Wire format mirrors lib/crypto.js exactly (<iv-hex>:<tag-hex>:<ct-hex>, optional ENC: prefix), so every existing encrypted credential continues to decrypt unchanged after cutover; smoke-tested both directions against the live .secrets/credential_key. Audit log at /var/lib/koda-keysvc/keysvc.log records every op (status, requestor, op). Rate limiter caps blast radius at 100 ops/sec (configurable via KEYSVC_MAX_OPS_PER_SEC). Idempotent setup-admin.sh creates the kodasvc user, /var/lib/koda-keysvc, copies the existing keyfile byte-for-byte (no key rotation), and loads com.koda.keysvc.plist into /Library/LaunchDaemons/. Ships in readiness state only — Phase 1 (sudo install) is BLOCKED on Klaus running keysvc/setup-admin.sh; Phase 3 (cutover: modify lib/crypto.js to call keysvc + restart Koda Core) is intentionally a separate follow-up so it lands AFTER keysvc is verified live. Inline eslint-disable on require('crypto') in keysvc.js is the deliberate Layer-1 exception — keysvc IS the new chokepoint owner of the AES-GCM key in a separate trust domain. |
| 30 Apr 20263:30 pm |
koda-core |
2.74.0 |
EIP Q1 #18 — Google Calendar reference bundle (first end-to-end exercise of the EIP Layer 3 pipeline). New lib/calendar.js chokepoint owns the googleapis dep (v171.4.0); lazy JWT auth via google.auth.JWT using the service-account JSON key loaded from cred store domain google-calendar-service-account-json (decrypted via lib/crypto), with concurrent-call coalescing on the auth handshake. Five public actions: listCalendars, listEvents, createEvent, updateEvent, deleteEvent — each emits an audit_log row via lib/audit.logAction (action prefix calendar.*, channel=autonomous, three-actor model with directedBy=requestor). Service-account-with-calendar-sharing model — works on personal Gmail (no Workspace, no DWD); Klaus shares each calendar with the service-account email manually. New bundles/calendar.yaml (parses clean via lib/bundles/parser): 5 actions, 2 permissions (calendar.read/write), delete_event marked destructive, full anti-pattern docs in instructions block (do-not lists for googleapis-direct, gcalcli, ID caching, bulk-delete-without-confirm). docs/chokepoints.json updated — lib/calendar.js no longer status: planned, managed_credentials corrected to google-calendar-service-account-json, rationale points at the new bundle. ESLint chokepoint ban now active for googleapis (verified: require('googleapis') outside lib/calendar.js fails lint). Phases 1+2 (GCP service-account setup + calendar sharing + cred upload) are owner actions and remain blocked pending Klaus' setup; the chokepoint surfaces a precise error when the cred is missing, pointing back to the instruction file. |
| 30 Apr 20263:00 pm |
koda-core |
2.71.0 |
EIP Week-2 #11 (audit H5) — `/db` PostgREST proxy deleted from server.js. The localhost-only passthrough was unused dead code: console PWA already routes through named `/api/*` endpoints (apps/console/js/api.js), no scripts called it, and the only test was the proxy's own auth-check assertion. All internal DB access now flows exclusively through lib/postgrest.js (cache + retry + audit). Updated tests/security/server-endpoints.test.js to assert the route is absent (`grep app\.(get|post|...)\(['"]/db['"]` returns nothing). Audit doc strikethrough markers added for H5 + §4.2 bypass row + §4.4 logging gap. |
| 30 Apr 20262:15 pm |
koda-core |
2.73.0 |
EIP Week-2 #13 (audit M5 / Layer 4 alerting) — anomaly watchdog cron + 4 starter rules over audit_log. New docs/anomaly-rules.json declares rules as (label, lookback_minutes, postgrest_query, threshold, severity, optional active_hours) tuples; queries adapted to the real audit_log schema (timestamp / app_name / outcome — not the strawman ts/integration/success/destructive columns from the instruction draft). New scripts/anomaly-watchdog.js: reads rules JSON each tick, HEAD-queries PostgREST with Prefer: count=exact, parses Content-Range total, fires DM via signal-notify.sh on threshold breach, suppresses re-alerts for 30min via tmp/anomaly-state.json (rule state cleared once count drops below threshold so next firing alerts immediately). 4 starter rules: auth-failure-burst (5+ in 10min), scheduler-failures (3+ in 1h), destructive-action-off-hours (1+ in 30min, 22:00-06:00 only, action ilike *delete*/*destroy*/*drop*/*wipe*/*reset*), audit-volume-spike (500+ in 5min). Two chokepoint bypasses documented in docs/chokepoints.json (postgrest + signal — cron must work when Koda Core is down). Registered in scheduled_tasks (id=6, */5 * * * *) and live scheduler picked it up via API PATCH (next_run_at populated). Smoke-tested via --dry-run: all 4 rules executed cleanly against live PostgREST, state file written. |
| 30 Apr 20262:05 pm |
koda-core |
2.72.0 |
EIP Week-2 #12 (audit M3) — integration registry + `/status integrations` + real chokepoint linter. (1) docs/chokepoints.json schema extended with health_endpoint, expected_health_status, related_scripts, managed_credentials per chokepoint, plus a top-level mcp_servers section enumerating koda-transcribe / stitch (stdio MCPs from ~/.claude.json) and playwright-mcp (long-running launchd plist). (2) New lib/integrations.js exposes loadRegistry / listIntegrations / listMcpServers / checkHealth / checkAllHealth (parallel fetch with per-call AbortController timeout, default 3s; mtime-aware cache so hot edits to chokepoints.json take effect without restart). (3) lib/commands/status.js adds an `integrations` subcommand that runs checkAllHealth in parallel and renders a tabular response with healthy/degraded/down/unknown icons plus the declared MCP server list. (4) scripts/lint-chokepoints.js rewritten from stub to a real Node-native walker (no rg dependency): scans .js/.ts/.mjs/.cjs/.sh files for owns_urls (string match) and owns_commands (subprocess-call heuristic — only flags a quoted command appearing as the first argument to spawn/spawnSync/exec/execSync/execFile* calls, plus a shell-syntax heuristic for .sh files); skips node_modules, .git, .venv, vendored skill repos under skills/_repos and skills/excalidraw-diagram/references, instructions/archive, tests, generated site/. (5) 30 legitimate bypasses documented in chokepoints.json (lib/config.js declarations, scripts that run when Koda Core is down, server.js bootstrap constants, and the WAHA URL-string collision). Linter runs clean: 0 violations across 207 scanned files. |
| 30 Apr 20261:30 pm |
koda-core |
2.71.1 |
Two work-auto resume fixes. (1) server.js /internal/work-continue endpoint now treats groupId === OWNER_UUID as an owner DM (leaves dataMessage.groupInfo null) instead of forcing groupInfo and getting silently dropped by the activation gate — mirrors how /internal/plan-continue already handles this case. (2) scripts/self-modify.sh confirmation/auto-resume logic restructured: confirmation message is picked once from any source (CONFIRM_MSG / SM_CONFIRM_MSG / temp file / generic), then the work-auto state file is checked unconditionally, with a 72h staleness threshold. Fresh queues auto-resume via /internal/work-continue with the queue notice appended to the chosen message. Stale queues prompt the owner instead of resuming, on the assumption that 72h+ idle means the queue likely completed and cleanup was missed. Also fixes .husky/pre-commit hook with --no-warn-ignored so staged tests/** files don't trip the eslint 'file ignored' meta-warning (preserves --max-warnings=0 chokepoint enforcement; only suppresses the meta-warning class). |
| 30 Apr 202612:15 pm |
koda-core |
2.70.0 |
EIP Week-1 #8 (H4) — WAHA media reads moved behind the WhatsApp channel chokepoint. Added getMediaFile()/fetchInboundMedia() to lib/channels/whatsapp.js (owns WAHA_MEDIA_ROOT, /api/files URL parser, localhost:3000→host rewrite, retry loop, /messages/{id}/download fallback). server.js webhook handler replaced ~70 lines of inline fs.readFileSync/axios logic with one channels.whatsapp.fetchInboundMedia(payload) call — server.js no longer references waha-media/ or WAHA storage layout. |
| 30 Apr 20268:48 am |
det22 |
1.9.0 |
EIP Week-1 #5 (C1) — proper CF Access JWT signature verification. Worker middleware no longer hand-decodes the JWT body via atob(); instead routes through new shared chokepoint apps/_shared/worker/cf-access-jwt.js which uses jose.jwtVerify against a 1h-cached remote JWKS (createRemoteJWKSet) and validates issuer (https://koda-systems.cloudflareaccess.com) + per-app audience (CF_ACCESS_AUD secret). Closes header-spoofing attack class — even if the worker URL becomes reachable directly, fabricated JWTs are rejected on signature mismatch. |
| 30 Apr 20268:48 am |
usermgmt |
1.7.0 |
EIP Week-1 #5 (C1) — proper CF Access JWT signature verification via shared apps/_shared/worker/cf-access-jwt.js chokepoint. Removed inline atob() body-only decoding from worker middleware; now full JWKS signature + issuer + audience validation via jose. |
| 30 Apr 20268:48 am |
koda-dashboard |
1.3.0 |
EIP Week-1 #5 (C1) — proper CF Access JWT signature verification via shared apps/_shared/worker/cf-access-jwt.js chokepoint. Replaced three call sites (middleware authenticate, serveDashboard, proxyProfile) that previously hand-decoded the JWT body with atob() — all now go through verifyAccessJwt() which performs full JWKS signature + issuer + audience validation via jose. |
| 30 Apr 20268:45 am |
koda-core |
2.69.0 |
EIP Day-1 #3 — ESLint + husky precommit hook for chokepoint enforcement (External Integration Pattern Layer 2). New eslint.config.js (flat config) reads docs/chokepoints.json and bans direct require()/import of owned modules outside the chokepoint owner via no-restricted-imports + no-restricted-syntax (covers CommonJS). New .husky/pre-commit runs `eslint --max-warnings=0` on staged JS files and runs scripts/lint-chokepoints.js stub (URL/command checker — fleshed out in EIP Week-2 #12). Initial known violations annotated with eslint-disable comments referencing fix-tasks (most are non-credential crypto uses; AES-GCM bypasses point to EIP Week-1 #7). Hook tested with deliberate violation. Added npm scripts lint, lint:fix, lint:chokepoints. Devdeps: eslint@^9, husky@^9, lint-staged@^15. |
| 28 Apr 20269:33 pm |
koda-core |
2.68.0 |
/work auto now auto-resumes across bridge restarts. Closes the gap where a restart mid-queue dropped you out and required a manual /work start. New /internal/work-continue endpoint (localhost-only, mirrors /internal/plan-continue) reads the group-scoped tmp/koda-work-auto-{groupId}.json state file and re-fires /work auto via a synthetic envelope through processMessage. scripts/self-modify.sh's verify-script branch that previously printed 'Send /work start to continue' now extracts the groupId from the state file, sends a 'Auto-resuming in 3s...' notification, and POSTs to the new endpoint. Falls back to the manual-resume notice on HTTP failure or missing groupId. |
| 28 Apr 20269:30 pm |
koda-core |
2.67.0 |
Group Listening Mode — opt-in ambient context backlog for @-mentions. New `/listening` command (admin+ to mutate, anyone to view) toggles per-group with `on`/`off`/`<N>`/`status`. When enabled, the last N messages OR last T minutes (whichever fewer, default N=25 / T=120m, hard cap 200) are fetched, deduped against the current invocation, and prepended as a `<conversation_context>` block in the system prompt. Speakers are resolved to display names (Koda's own = 'Koda', unregistered = last 4 phone digits). Attachments rendered as `[image]` / `[voice]` / `[file: name]` / `[sticker]` placeholders. Quote-replies trigger a depth-5 chain walk that surfaces older messages even when outside the time window. Privacy gate: `groups.listening_enabled_at` ensures pre-enable messages never surface (re-enable resets the gate). Anti-injection framing instructs Claude to use the block as background only and act exclusively on the final message. Schema: `groups.listening_n`, `groups.listening_t_minutes`, `groups.listening_enabled_at`. Implementation: `lib/context-backlog.js` (reader/dedupe/formatter), `lib/commands/listening.js` (command), `server.js` wiring before invokeClaudeCode. |
| 28 Apr 20269:02 pm |
koda-core |
2.66.0 |
/work command now surfaces standalone instruction files (those not part of any *-queue.md) alongside queue-managed tasks. Each standalone file with a task-style metadata header (`> **Depends on**:` or `> **Modifies**:`) appears as a virtual single-task queue in /work status, /work list, /work next, and is executable via /work start. On successful completion the instruction file is auto-archived to instructions/archive/, matching the queue-completion convention. Closes the gap where single-task work was invisible to /work. |
| 28 Apr 20268:12 pm |
koda-core |
2.65.1 |
Fix: group-removal deactivation now checks Koda's actual membership, not just group-ID presence in the upstream roster. signal-cli keeps groups in its local cache after you're kicked (only the members array updates), so the previous logic never fired for Signal kicks. Now matches config.SIGNAL_PHONE against g.members for Signal and config.WHATSAPP_BOT_NUMBER against g.participants[].phoneNumber for WhatsApp. Falls back to ID-presence for older WAHA builds without participants data. |
| 28 Apr 20267:59 pm |
koda-core |
2.65.0 |
Owner-only /leave command with two-step confirmation for Koda to leave Signal or WhatsApp groups (channel-aware: signal-cli quit endpoint or WAHA leave endpoint). Plus auto-deactivation: syncGroupNames now polls both signal-cli and WAHA every 15 min and marks any koda_active=true group missing from its channel's roster as inactive, audit-logs the event, and DMs the owner. False-positive guard: deactivation only fires on a successful upstream fetch, so API errors/reconnect blips don't cause false positives. |
| 28 Apr 20267:05 am |
koda-core |
2.64.0 |
Group member roster injection + three-layer identity-action security. Group chats now get a Signal-sourced member roster in the system prompt (awareness only). Three code-enforced gates fire before any koda-intent dispatches: Layer 2 verified-target (manifest params.verified must come from @-mention or literally-typed UUID/phone), Layer 3a requestor permission (enforcePermission, fail-closed), Layer 3b cascading role (canManageRole) for role grants. Manifest schema additions: params.verified[], permission, cascading_role. Applied to usermgmt (update_user, assign_role) and det22 (rsvp, set/query/clear_availability). Full design: docs/identity-action-security.md. |
| 27 Apr 20268:30 pm |
det22 |
1.8.0 |
Admin panel now opens as an embedded modal (iframe to users.koda.systems?app=det22&embed=true) instead of a new tab — user stays in DET22 context. Added shared AdminModal component in apps/_shared/. Source task #243. |
| 27 Apr 20268:30 pm |
usermgmt |
1.6.0 |
Embed mode (?embed=true): hides header/tabs/profile, transparent background, posts events to parent on role-changed/user-invited/user-deleted/close. CSP frame-ancestors header allows iframing from *.koda.systems. Enables embedding inside DET22 and other T2 apps. Source task #243. |
| 27 Apr 20266:15 pm |
koda-core |
2.63.1 |
Voice-note transcription fidelity bump: base.en → small.en (~30% lower WER on conversational English) and added initial prompt biasing decoder toward Koda proper nouns (Koda, Wandi, DET22, koda.systems, Klaus). Latency unchanged for short clips. |
| 27 Apr 20266:05 pm |
koda-core |
2.63.0 |
Signal voice-note ingestion (DM only): single audio attachment → whisper.cpp base.en → transcript injected into chat pipeline. Fully offline, ~1.5s for short clips. Heavy koda-transcribe MCP untouched. Source task #275. |
| 27 Apr 202610:30 am |
koda-console |
1.37.0 |
In-app notification badges: red badge dots on Tasks (new tasks), Chat (unread), and More tab (system events). Badges on hamburger menu items and quick-switch dropdown. localStorage-based last-seen tracking (#268). |
| 27 Apr 202610:00 am |
koda-console |
1.36.1 |
Post-sprint fixes: iOS auto-zoom prevention, per-session draft persistence, chat race condition fixes (abort vs error, session-scoped typing indicator), SW cache bump. |
| 27 Apr 202610:00 am |
koda-core |
2.61.1 |
Wire logSystemEvent() into startup/shutdown hooks, store sessionId in activeClaudeSessions, return sessionId from /api/chat/active. |
| 27 Apr 20269:36 am |
koda-console |
1.38.0 |
Per-user per-channel model selector: tappable model pill on dashboard opens bottom sheet with all available models (versioned, with multiplier badges). Selection persisted server-side per user per channel. |
| 27 Apr 20269:36 am |
koda-core |
2.62.0 |
Version-level MODEL_REGISTRY with per-model multipliers. Per-user per-channel model preferences (user_model_preferences table). refreshModels merges CLI + registry. API returns versioned model list with user preference. |
| 26 Apr 20262:30 pm |
koda-console |
1.36.0 |
Console PWA sprint: Apps management section (view, rename, suspend/resume), System Messages panel with type filters, new menu items in More tab (#233, #268, #272). |
| 26 Apr 20262:30 pm |
koda-core |
2.61.0 |
PATCH /api/apps/:name endpoint, /app rename/suspend/resume commands, system_events table + GET/DELETE API, route mounting (#232, #233, #272). |
| 26 Apr 202611:38 am |
koda-core |
2.60.3 |
Opus 4.7 migration: fix Opus pricing in MODEL_INFO ($5/$25 MTok, 1.67x multiplier), update CLI to 2.1.112, update vscode-orchestrator skill references. |
| 26 Apr 20269:33 am |
koda-core |
2.60.2 |
Add watchdog health checker (scripts/watchdog.js) with dual-channel alerting (Signal + Resend email). Checks Claude CLI auth, disk space, service ports, Docker containers, and image updates. Scheduled every 15 min (#73). |
| 26 Apr 20268:40 am |
koda-console |
1.35.2 |
Restore typing indicator when navigating back to an active chat — polls /api/chat/active and shows 'Claude is typing...' until the response completes. |
| 26 Apr 20268:40 am |
koda-core |
2.60.2 |
Add GET /api/chat/active endpoint to check if Claude is currently processing a response. Also fix missing planFilePath import in /continue command. |
| 25 Apr 202610:30 pm |
koda-core |
2.60.1 |
Fix PWA chat auto-naming bug: scope session-id backfill to current request only, preventing old orphaned messages from contaminating new session titles (#273). |
| 25 Apr 202611:52 am |
koda-core |
2.60.0 |
Persist message queue across restarts: save queued messages to disk on shutdown, restore and re-process on startup. Also fixes session loss during intentional restarts by draining active Claude sessions before exit. |
| 24 Apr 20262:00 pm |
deploy-watch |
1.1.1 |
Code audit fixes: pinned Docker images with SHA digests, added healthchecks, fixed subshell variable scoping in fswatch loop, atomic lockfile for prune-logs, self-modify.sh mkdir lock. |
| 24 Apr 20262:00 pm |
koda-console |
1.35.1 |
Code audit fixes: pinned Tailwind CDN to v3.4.1, restored viewport accessibility zoom, Alpine x-for unique keys. |
| 24 Apr 20262:00 pm |
koda-core |
2.59.0 |
Codebase audit: ~80 fixes across security (path traversal, auth, injection), performance (sync I/O, memory leaks, N+1 queries), race conditions (lockfiles, concurrency), API hardening (ownership checks, role escalation, input validation), and code quality (shared modules, deduplication). |
| 24 Apr 20261:20 pm |
koda-core |
2.59.1 |
Fix usage scraper: switch to playwright-extra with stealth plugin (non-headless) to bypass Cloudflare bot detection on claude.ai. |
| 23 Apr 202612:00 pm |
koda-core |
2.58.0 |
Multi-channel abstraction: conversational paths now channel-agnostic, control-plane notifications configurable (ADR-020). |
| 22 Apr 20269:04 pm |
koda-console |
1.35.0 |
Stale-while-revalidate caching for dashboard and tasks tabs — app opens instantly with cached data, refreshes in background (#269). |
| 22 Apr 20269:00 pm |
koda-console |
1.34.1 |
Chat timeout errors now show as visible messages in the conversation instead of brief toasts. |
| 22 Apr 20269:00 pm |
koda-core |
2.57.1 |
Chat SSE error events now include session ID for client recovery after timeout. |
| 22 Apr 20267:19 pm |
koda-core |
2.57.0 |
Add /btw slash command — side-channel questions that bypass the message queue, with forked session context via --resume --fork-session (#192). |
| 22 Apr 20264:20 pm |
koda-dashboard |
1.2.0 |
Profile menu now shows editable First Name / Last Name fields (shared ProfileMenu component extended). Fixed null names from broken onboarding proxy. |
| 22 Apr 20262:30 pm |
koda-dashboard |
1.1.0 |
Onboarding flow with name confirmation, guided tour, profile menu, group chat indicator on tiles, URL renamed to dash.koda.systems (#206). |
| 22 Apr 20262:30 pm |
usermgmt |
1.5.0 |
Self-service profile: first_name/last_name columns, GET /api/me returns names, PATCH /api/me for self-service name updates (#206). |
| 22 Apr 20262:30 pm |
koda-core |
2.56.0 |
Dashboard entitlements sync now includes has_group_chat from workspace_surfaces (#206). |
| 22 Apr 202610:35 am |
usermgmt |
1.4.0 |
Email invite notifications: auto-send welcome email on user creation, email-linked notification on proxy email linking, resend invite endpoint + UI badge (#261). |
| 22 Apr 20269:44 am |
det22 |
1.7.2 |
Replace freeform category input with pill selector (Range/CAPDEV/OAI/Skills) (#267). |
| 21 Apr 202610:50 pm |
koda-core |
2.55.0 |
Add /api/mcp-status endpoint for MCP server health (#265). |
| 21 Apr 202610:50 pm |
koda-console |
1.34.0 |
Add MCP server status pills to dashboard card (#265). |
| 21 Apr 202610:50 pm |
dashboard-generator |
1.11.0current |
Add MCP servers section to koda.systems status page (#247). |
| 21 Apr 202610:21 pm |
dashboard-generator |
1.10.1 |
Add component versions section to the build page (#94). |
| 21 Apr 202610:12 pm |
usermgmt |
1.3.0 |
Auto-sync CF Access policy on user create/delete; order role dropdown most→least privileged (#34, #250). |
| 21 Apr 20269:48 pm |
koda-console |
1.33.1 |
Fix PWA chat lifecycle persistence: save state on visibilitychange/pagehide, persist and restore draft text (#210, #235). |
| 21 Apr 20265:14 pm |
usermgmt |
1.2.0 |
Show app display_name instead of slug in role picker and user detail view (#259). |
| 21 Apr 20265:14 pm |
koda-console |
1.33.0 |
Pre-select app scope in create-task expanded form when filter pill is active (#253). |
| 21 Apr 20265:14 pm |
koda-core |
2.54.0 |
Fix HOT_RELOADABLE set (remove destructured modules) and await /model DB writes (#209, #217). |
| 21 Apr 20264:40 pm |
koda-core |
2.53.0 |
Save scheduled task sessions to session_history so they appear in /resume. |
| 20 Apr 20269:00 pm |
det22 |
1.7.1 |
Fix cell tap interaction: click for single-tap edit, pointer events only for drag-paint, pointer-events:none on child elements. |
| 20 Apr 20268:15 pm |
det22 |
1.7.0 |
Availability UI: toolbar restructure (edit top-left, view/range swap on right), sticky month headers, readable aggregate counts. |
| 20 Apr 20267:58 pm |
koda-core |
2.52.1 |
Inject sender identity (created_by + directed_by) into intent params — Koda acts as EA, directing user shown as creator. |
| 20 Apr 20267:55 pm |
det22 |
1.6.2 |
Resolve created_by email to user_id for directed-by attribution on service-token POSTs. |
| 20 Apr 20267:15 pm |
det22 |
1.6.1 |
Fix POST crash on service token auth: null created_by fallback to email/koda label. |
| 20 Apr 20267:05 pm |
koda-core |
2.52.0 |
Fix intent pipeline: CF Access + service token auth from credential store, auto-execute high-confidence (>=0.9) intents as batch without per-item confirmation, multi-pending intent support. |
| 20 Apr 20266:00 pm |
det22 |
1.6.0 |
Edit mode toggle for availability (off=scroll freely, on=drag-paint). Scroll overshoot fix with 300ms cooldown. Admin icon replaced with settings cog. Tasks #244, #249, #251. |
| 20 Apr 20265:00 pm |
det22 |
1.5.0 |
RSVP counts centered under status pill. Light/dark theme toggle with system preference detection. Fix tile gap. Desktop responsive layout with 2-col grid. |
| 20 Apr 20264:00 pm |
det22 |
1.4.0 |
Card layout: meta chips + RSVP summary on same row. Calendar range picker replaces dual date inputs. Remove summary tile gap. Usermgmt DET22 naming fix. |
| 20 Apr 20264:00 pm |
usermgmt |
1.1.1 |
Fix app slug placeholder to show DET22 (uppercase) consistently |
| 20 Apr 20269:03 am |
koda-core |
2.51.0 |
Enforce session ID capture for scheduled scripts: scripts emit KODA_SESSION_ID, scheduler parses and warns if missing (expects_session flag) |
| 19 Apr 20268:30 pm |
det22 |
1.3.0 |
Card layout overhaul: RSVP summary on right (1/0/0 format), condensed same-month dates, notes+actions bottom row, person icon avatar, drag handle touch areas, reduced header spacing |
| 19 Apr 20267:50 pm |
det22 |
1.2.0 |
RSVP toggle: tapping already-selected status removes RSVP. Date fields stack vertically on mobile. Playwright test helper (pw-browse.js). |
| 19 Apr 20267:20 pm |
det22 |
1.1.3 |
Fix date fields overflowing modal on iOS — add min-width:0 to flex date inputs, prevent horizontal scroll |
| 19 Apr 20267:00 pm |
det22 |
1.1.2 |
Fix User Management link URL from usermgmt.koda.systems to users.koda.systems in profile menu |
| 19 Apr 20265:30 pm |
koda-core |
2.50.0 |
Integrate User Management app: usermgmt-client.js, /app sync delegates to usermgmt API with Postgres fallback, config vars for USERMGMT_API_URL/SERVICE_TOKEN |
| 19 Apr 20265:15 pm |
usermgmt |
1.1.0 |
Derived access model: app admins get implicit scoped access without explicit role. Post-create role assignment flow. Proxy user checkbox. Audit log endpoint. Filtered app picker. |
| 19 Apr 20262:00 pm |
usermgmt |
0.1.0 |
Scaffold User Management app: D1 database, wrangler config, worker boilerplate, frontend shell, shared components |
| 19 Apr 20261:08 pm |
usermgmt |
1.0.0 |
First production release: deployed to users.koda.systems, D1 schema, user CRUD, role management, sync push to Tier 2 apps with CF Access bypass, app-aware theming, PWA frontend |
| 19 Apr 202610:58 am |
koda-core |
2.49.1 |
/work next stashes queue context so /work start and /work auto continue without re-specifying the queue |
| 19 Apr 202610:37 am |
det22 |
1.1.1 |
Desktop availability calendar: constrain grid to 600px max-width, centre layout, improve spacing |
| 19 Apr 202610:30 am |
det22 |
1.1.0 |
Frontend overhaul: tappable summary tiles as filters, search bar, DET22 header with view subtitle, profile sheet slide animation + name edit tick/cross, contextual return-to-today, FAB sizing |
| 19 Apr 20269:33 am |
koda-core |
2.49.0 |
Scheduled tasks capture session ID from prompt-type runs and include resume command in notifications |
| 18 Apr 20266:20 pm |
det22 |
1.0.1 |
Rename app from det22-activity-tracker to det22 — directory, manifest, DB, wrangler, all references |
| 18 Apr 20265:07 pm |
koda-core |
2.48.0 |
Add /wandi slash command (pause/unpause/status), fix 3-step unpause flow, document ADR-019 (script wrappers as commands not apps) |
| 18 Apr 20262:10 pm |
koda-console |
1.32.0 |
Dynamic app list in task forms — fetches registered apps from koda_apps API instead of hardcoded Console-only dropdown |
| 18 Apr 202612:00 pm |
det22 |
1.0.0 |
Add profile menu, remove System tab (system info moved to profile sheet), clean up header UI |
| 18 Apr 202612:00 pm |
koda-console |
1.31.0 |
Add reusable profile menu component (avatar, popover, profile sheet) replacing plain text user display in header |
| 18 Apr 20269:30 am |
koda-console |
1.30.0 |
Pull-to-refresh: Facebook-style circular arrow indicator, disabled in chat conversation threads, spins in place until refresh completes. SW version now tracks console version. |
| 17 Apr 202611:26 am |
koda-console |
1.29.1 |
Fix cross-channel session contamination: moved session resume from separate pre-flight API call to inline metadata passed with send request. |
| 17 Apr 202611:26 am |
koda-core |
2.47.1 |
Fix cross-channel session contamination: send endpoint handles session resume inline, waits for active Claude invocations before swapping sessions. Prevents PWA resume from disrupting in-flight Signal conversations. |
| 17 Apr 202611:18 am |
koda-core |
2.47.0 |
Capability gates for transcription and document generation: added transcribe + generate_document to grantable capabilities, system prompt now explicitly enforces capability checks on all gated features. Published role-capability-matrix.html to koda.systems. |
| 17 Apr 202610:50 am |
koda-core |
2.46.2 |
Rich role-assignment confirmation: /user role and /user grole now show assigned role, active LLM, permissions summary, and full workspace access list (#230). |
| 17 Apr 202610:36 am |
koda-console |
1.29.0 |
PWA bug batch: fix stream session contamination (#228), streamed messages on re-entry (#212), send lock scoped to session (#213), blank push notifications (#215), newline preservation (#218), input re-fill on resume (#226), autoGrow scroll jump (#229), suppress push when foreground (#211). |
| 17 Apr 202610:36 am |
koda-core |
2.46.1 |
Sanitize push notification preview text: strip markdown formatting, handle blank/whitespace content (#215). |
| 17 Apr 202610:02 am |
koda-console |
1.28.0 |
Create-task form now supports attachments: stage files before task creation, uploaded automatically after the task is created (task #227). |
| 16 Apr 202611:10 am |
koda-console |
1.27.0 |
Task attachment UI: upload files to tasks, view thumbnails, delete attachments from task edit modal. |
| 16 Apr 202611:10 am |
koda-core |
2.46.0 |
Task attachments feature: task_attachments table, REST API endpoints (upload/list/serve/delete), Signal+WhatsApp attach-to-task detection with confirmation messages. |
| 16 Apr 202610:50 am |
vscode-bridge |
0.2.5 |
Add timestamps to all log output, add /ready deep health check endpoint (verifies Copilot can actually respond, not just that HTTP server is up). New vscode-orchestrator skill for delegating build work. |
| 15 Apr 202612:37 pm |
koda-core |
2.45.1 |
Fix WhatsApp inbound media attachments: add WHATSAPP_FILES_MIMETYPES=* for WAHA auto-download, fallback to /api/default/messages/{id}/download endpoint when media.url 404s, debug logging for media objects. |
| 15 Apr 20269:25 am |
koda-core |
2.45.0 |
Multi-mention guard: reject commands with >1 @mentioned user (returns clear error instead of guessing). Updated Group Chat Guide and RBAC diagram for multi-channel (Signal + WhatsApp), documented one-workspace-one-channel design decision. |
| 14 Apr 20266:30 pm |
dashboard-generator |
1.10.0 |
Refactor Flow page into Reference Docs with category tabs (Architecture/Reference/Process). Architecture tab keeps version selector; Reference and Process tabs show current-only content. Consolidated task lifecycle into Process tab. Updated nav links across all pages. |
| 14 Apr 20265:50 pm |
koda-core |
2.44.0 |
Two-layer RBAC: workspace-scoped roles with global floor. Effective role = max(global, workspace). Context-aware /user commands with @mention support in groups, explicit IDs in DMs. New: /user grole (global role), /user remove, /user workspaces, auto-revoke on group leave, /activate auto-creates workspaces, workspace-scoped capabilities. Replaced checkBinaryGate with getEffectiveRole. |
| 14 Apr 202612:20 pm |
koda-core |
2.43.0 |
WhatsApp channel integration via WAHA (NOWEB engine). New lib/channels/ abstraction: unified send() router for Signal and WhatsApp, webhook endpoint for inbound WhatsApp messages, channel-aware processMessage pipeline, session auto-start, message formatting per-channel, LID (Linked ID) addressing support for WhatsApp Web protocol. Also: group activation gate (Koda inert in unactivated groups). |
| 13 Apr 20266:45 pm |
dashboard-generator |
1.9.1 |
Standardise nav link positioning across all pages (separate nav-links from page-meta divs). Add ambient corner orbs to all pages via sharedCSS() using per-page accent colours. New koda-site-design-system skill. |
| 13 Apr 20266:15 pm |
koda-console |
1.26.1 |
Standardise Chat session header to match section label pattern (text-xs uppercase). New pwa-design-system skill documenting typography scale, colour palette, and component patterns. |
| 13 Apr 202610:36 am |
koda-core |
2.42.0 |
Web Push notifications for Console PWA: VAPID key management, push_subscriptions table, /api/push endpoints (subscribe/unsubscribe/test/status), service worker push+notificationclick handlers, client subscription flow with iOS re-subscribe, Settings toggle UI. Pushes only when user has no active SSE/long-poll connection. |
| 12 Apr 20269:20 pm |
dashboard-generator |
1.9.0 |
v3-10 documentation: version tabs (v1/v2/v3) on build page, v3.0 checklist tracking from checklist-v3.json, architecture diagrams (4 HTML→PNG via Playwright), v3 install notes, updated landing page description. |
| 12 Apr 20268:05 pm |
koda-console |
1.26.0 |
Audit log view in More tab with filters (app, actor, channel, date range), actor-type badges (user-direct/koda-directed/koda-autonomous), and detail JSON expansion. |
| 12 Apr 20268:05 pm |
koda-core |
2.41.0 |
Three-actor audit trail: lib/audit.js module, audit_log schema extensions (directed_by, channel, app_name), Tier 2 audit collection in lib/sync.js, /app audit command, GET /api/audit endpoint. |
| 12 Apr 20267:45 pm |
koda-core |
2.40.0 |
v3.0 Tier 2 app framework: updated scaffold-app.sh with admin API/permissions/audit templates, deploy-app.sh with secrets/sync/health verification, lib/sync.js for permission push and health monitoring, /app command handler, daily reconciliation script. |
| 12 Apr 20267:06 pm |
dashboard-generator |
1.8.0 |
Add Open Dashboard CTA button to koda.systems landing page, linking to dashboard.koda.systems. |
| 12 Apr 20266:35 pm |
koda-core |
2.39.7 |
Fix work queue post-restart check race: also match 'interrupted' status since server.js startup changes auto-queue state before verify script runs. |
| 12 Apr 20266:05 pm |
koda-core |
2.39.6 |
Work queue restart UX: post-restart message now detects active work queues and tells owner to send /work start to continue, instead of generic 'restarted and verified'. |
| 12 Apr 20265:40 pm |
koda-core |
2.39.5 |
Work queue resume fix: findNextTask() now prioritises in_progress tasks over pending, so interrupted queue items are resumed after restart instead of skipped. |
| 12 Apr 20265:25 pm |
koda-core |
2.39.4 |
Work queue progress tracking: /work auto sessions now mark completed steps with COMPLETED markers and tick verification checkboxes in instruction files, enabling clean resume after restarts. |
| 12 Apr 20265:05 pm |
koda-core |
2.39.3 |
Startup cleanup: auto-archive completed plan files to prevent stale plans confusing new sessions; detect interrupted /work auto runs and notify owner to resume. |
| 12 Apr 20261:12 pm |
koda-core |
2.39.2 |
Capture Signal send timestamps for outbound message_id; enables quote lookup of Koda replies via exact message_id match instead of content fallback. |
| 12 Apr 202610:24 am |
koda-core |
2.39.1 |
Signal quote context: look up full original message from DB instead of truncated 500-char signal-cli text; add quoted_message_id column to messages table; 4000-char fallback limit. |
| 11 Apr 20269:50 am |
koda-core |
2.39.0 |
RBAC v2: four-role hierarchy (owner/admin/operator/viewer), per-workspace permission resolution, binary gate, cascading role management, workspace user API endpoints, Signal workspace role commands. |
| 11 Apr 20268:19 am |
dashboard-generator |
1.7.2 |
Add v2.0 and v3.0 spec document links to build page Software card; copy v3.0 spec to site directory. |
| 10 Apr 202610:54 pm |
koda-console |
1.25.2 |
Fix edit modal: prevent horizontal overflow, stack priority/date for mobile, use separate availableGroups for form dropdowns (regression from scope pills change). |
| 10 Apr 202610:40 pm |
koda-console |
1.25.1 |
Label taxonomy: approved label chip grid in add/edit forms (bug, feature, maintenance, docs, core, pwa, infra, next-release), custom label fallback preserved. |
| 10 Apr 20269:56 pm |
koda-console |
1.25.0 |
Task filtering: scope pills built from task data (global + app + group), label multi-select OR-filter with toggleable pills, fix template syntax bug (#193 #194 #195). |
| 10 Apr 20269:47 pm |
koda-core |
2.38.2 |
Fix plan mode names in system prompt: auto→semi, full→auto to match actual /continue command modes. |
| 10 Apr 20265:00 pm |
koda-core |
2.38.1 |
Remove stale console.koda.systems from CORS whitelist and dead-end migration comment. |
| 10 Apr 20265:00 pm |
koda-console |
1.24.2 |
Fix manifest URL from dead console.koda.systems to api.koda.systems/console/. |
| 9 Apr 20268:00 pm |
koda-core |
2.36.0 |
Rewrite generatePDF() to use React-PDF pipeline (Mockup D v7 styled output) with Playwright fallback. Supports both structured DocumentContent objects and legacy markdown strings. Adds parseMarkdownToStructured() and structuredToMarkdown() helpers. |
| 9 Apr 20267:00 pm |
koda-console |
1.24.1 |
Detect Cloudflare Access session expiry: use redirect:'manual' on fetch to catch 302 redirects, show amber 'Session expired — tap to re-authenticate' banner instead of silent failure or misleading 'offline' state. |
| 9 Apr 20266:37 pm |
dashboard-generator |
1.7.1 |
Standardise nav bar across all generated pages: all 6 links (Home, Build, Status, Changelog, Flow, Tasks) with active indicator on each page. |
| 9 Apr 20266:37 pm |
koda-core |
2.38.0 |
Rename /continue modes: auto→semi, full→auto (aligns with /work auto naming). Add semi-auto alias. /work auto now prompts for queue selection when multiple active queues exist. Updated docs: SOP task lifecycle, information-flow (auto-queue diagram), task-lifecycle page. |
| 9 Apr 20265:38 pm |
koda-core |
2.37.1 |
Fix duplicate auto-queue notifications (replace signal-notify.sh with services.reply) and message ordering race condition (claimGroup in processNext before async dispatch, bypass queue gate for _fromQueue messages). |
| 9 Apr 20265:12 pm |
koda-core |
2.37.0 |
React-PDF document generation pipeline: professional PDF output with native gradients, Inter/Source Serif typography, inline SVG diagrams, Koda-branded template system. Replaces Playwright-based PDF generation (kept as fallback). End-to-end validated. |
| 9 Apr 20264:22 pm |
koda-core |
2.35.0 |
Unattended work queue execution: /work auto [queue] runs all eligible tasks back-to-back with progress notifications, prerequisite checking, queue status updates, and /stop integration. Refactored task execution into reusable helpers. |
| 9 Apr 202612:34 pm |
koda-core |
2.34.1 |
Fix CLI hang (10s watchdog after result event), browser leak in document-generator (try/finally), PDF fallback filename, extract Melbourne date helper, use fileToAttachment() in research pipeline, fix isGroup detection, parallel doc generation. |
| 9 Apr 202612:44 am |
koda-console |
1.24.0 |
Completed tasks: load all (no limit), search includes done tasks, load-more button. Faster chat polling (5s) during active plans for prompt system message display (#164, #165). |
| 9 Apr 202612:44 am |
koda-core |
2.34.0 |
Session-aware system notifications: /internal/notify endpoint routes messages to PWA session (DB) or Signal based on context. signal-notify.sh calls Core first with Signal API fallback (#165). |
| 9 Apr 202612:15 am |
koda-core |
2.33.0 |
Hybrid research: parallel sub-agent research with document generation pipeline. Timestamped filenames (research-slug-YYYY-MM-DD-HHmm.ext), URLs via file-share system, new research-publish.js script. |
| 8 Apr 20268:31 pm |
koda-console |
1.23.1 |
PWA UX fixes: bigger/brighter refresh icon in header (#161), dropdown arrow no longer clipped off-page (#162), tighter task list spacing (#163). |
| 8 Apr 20267:20 pm |
koda-console |
1.23.0 |
Fix session-bleed bug: back button now clears all session state, polling/streaming no longer fall back to stale session IDs, sessionStorage restore includes viewing session context. |
| 8 Apr 20267:06 pm |
koda-console |
1.22.1 |
Fix SW unregistered after force reload: log registration errors instead of silently swallowing, clean up ?cb= cache-buster query param from URL. |
| 8 Apr 20265:57 pm |
koda-core |
2.32.0 |
Suppress redundant startup/shutdown DMs during plan-driven restarts; improve self-modify restart messages with timing estimates, auto-mode differentiation, and cleaner CTAs (#125). |
| 8 Apr 20265:57 pm |
koda-console |
1.22.0 |
Enter to send on desktop chat (#158), remove unnecessary New Chat confirmation dialog (#159). |
| 8 Apr 20264:20 pm |
koda-console |
1.21.1 |
Fix chat rename for new sessions: populate chatViewingSession on session ID arrival, add pencil icon to header for rename discoverability. |
| 8 Apr 20264:08 pm |
koda-console |
1.21.0 |
Fix chat message display: slash commands show as command text (not expanded prompts), system messages persisted and styled distinctly, command messages get outlined bubble style. |
| 8 Apr 20264:08 pm |
koda-core |
2.31.0 |
Add msg_type column to messages table, accept displayAs param in chat send endpoint, persist slash command text and system messages to DB, return msgType in history API. |
| 8 Apr 20263:32 pm |
koda-console |
1.20.0 |
Fix PWA chat session isolation — pass explicit groupId in viewSession (#120). Add 'Work on this' button to instruction detail view (#130). |
| 8 Apr 20263:32 pm |
koda-core |
2.30.0 |
Fix chat rename bug: remove unsafe cross-session fallback in autoNameLatestSession, add overlap guard to history timestamp fallback, delay auto-naming for backfill (#120). Enhance auto-naming with Claude-generated titles for 10+ message sessions (#157). |
| 8 Apr 20261:41 pm |
koda-console |
1.19.1 |
Fix 3 PWA bugs: task list horizontal overflow on mobile, completed tasks not tappable, chat rename keyboard not appearing on mobile (user-gesture focus fix). |
| 8 Apr 20261:15 pm |
koda-core |
2.29.1 |
Branded 404 page for expired/invalid share links (CF Pages + Core route). Directory listing guard on site/shared/. |
| 8 Apr 202612:02 pm |
koda-console |
1.19.0 |
Add Shared Files section to PWA hamburger menu with list, copy link, open, and delete actions. Add sharedFiles API client. |
| 8 Apr 202612:02 pm |
koda-core |
2.29.0 |
File sharing via web links (Task #151). Hybrid architecture: auto-share generated docs to CF Pages CDN, manual /share command for core-hosted files. New shared_files DB table, /api/shared-files REST API, /share/:token public download route, /share Signal command, auto-share in document-generator. |
| 8 Apr 202610:35 am |
koda-core |
2.28.1 |
Archive completed/stopped/cancelled plans to logs/plans/ instead of deleting. PWA poll auto-archives completed plans so banner clears. self-modify.sh archives after last step. /stop and /plan cancel also archive. |
| 8 Apr 20269:45 am |
koda-console |
1.18.0 |
PWA slash commands (/continue, /plan, /stop etc), plan progress banner with live polling, scroll-to-top floating button. Slash commands route through new POST /command endpoint. |
| 8 Apr 20269:45 am |
koda-core |
2.28.0 |
Add POST /api/chat/command and GET /api/chat/plan endpoints for PWA slash commands and plan status. Replace auto-continue ACK with informative plan-aware message. Registry.handle() accepts services override. |
| 8 Apr 20269:00 am |
koda-core |
2.27.6 |
Reduce AI response latency: remove 1.5s batch delay (BATCH_DELAY_MS=0), reuse earlySessionId to eliminate duplicate DB call, parallelise all pre-Claude lookups (instructions, memories, permissions, group type), add 60s in-memory cache for instructions and 30s cache for memories. Also fixed crash-loop from duplicate isOwner declaration. |
| 8 Apr 20267:48 am |
koda-console |
1.17.2 |
Fix cross-chat streaming bleed: scope streaming display (thinking/response/typing bubbles) to the originating session so navigating to another session no longer shows the wrong stream. |
| 7 Apr 202610:30 pm |
koda-core |
2.27.2 |
Queue ack quoting refined: only quote original message when actually queued (not on initial ack). Pre/post change notification rule added to CLAUDE.md. |
| 7 Apr 202610:15 pm |
koda-console |
1.17.1 |
Quick-win PWA batch: rename Running→Running Jobs, tick/cross icons on chat rename, scroll-to-bottom floating button in chat, tap chat tab again to return to session list. |
| 7 Apr 202610:00 pm |
koda-console |
1.17.0 |
Fix PWA chat state loss on iOS background/navigation: graceful stream error recovery preserves partial responses, visibilitychange handler re-fetches history on resume, sessionStorage buffering survives page kills. |
| 7 Apr 202610:00 pm |
koda-core |
2.27.5 |
Chat API emits sessionId as early SSE event before streaming, enabling client recovery when streams break mid-response. |
| 7 Apr 20269:41 pm |
koda-console |
1.16.2 |
Sort session list by last message activity instead of archive/creation time. Simplified time-ago display to use lastActivity field. |
| 7 Apr 20269:41 pm |
koda-core |
2.27.4 |
Session history API now returns lastActivity field (latest message timestamp per session) for accurate sort ordering. |
| 7 Apr 20269:35 pm |
koda-core |
2.27.3 |
Move quote from queue ack to dequeued processing ack — quote now appears when the queued message is actually picked up, not when it's queued. |
| 7 Apr 20269:07 pm |
koda-core |
2.27.1 |
Queue ack quotes the original message via Signal reply, so the user can see what was picked up when processing catches up. |
| 7 Apr 20269:02 pm |
koda-console |
1.16.1 |
Sort PWA chat session list by most recently replied (endedAt desc, with startedAt fallback for current sessions). |
| 7 Apr 20268:45 pm |
koda-console |
1.16.0 |
PWA app badge API (unread count on home screen icon), chatTransport routing fix (PWA→Signal leak), New Chat button moved to session list only, unread count pills in session list, markRead watermark wiring on view/tab-switch/poll. |
| 7 Apr 20268:45 pm |
koda-core |
2.27.0 |
Group-scoped plan files (/tmp/koda-plan-{groupId}.json) to prevent cross-session clobbering. Session reads table + mark-read endpoint + unread count computation in session history API. Scheduler prompt command type. plan-continue endpoint routes PWA vs Signal correctly. |
| 7 Apr 20261:50 pm |
koda-core |
2.26.1 |
Progressive flush hardening: drop dead subtraction logic, serialize concurrent drains via in-flight gate (prevents Signal message reordering), set progressClosed flag in finally to ignore late events from in-flight claude.js callbacks, broaden low-value regex to keep markdown headings and bullets, make verbose-mode debounce respect PROGRESS_FLUSH_DEBOUNCE_MS as a ceiling. |
| 7 Apr 20261:30 pm |
koda-core |
2.26.0 |
Progressive Signal flushes: narration text now reaches Signal at tool-use boundaries with debounced coalescing (PROGRESS_FLUSH_DEBOUNCE_MS, default 1500ms) instead of arriving as one wall after the run finishes. Low-value short segments are filtered. PROGRESS_VERBOSITY env (quiet|normal|verbose) controls behavior. Final completion summary still sent, with already-streamed prose subtracted to avoid duplication. |
| 7 Apr 202612:30 am |
koda-console |
1.15.0 |
Scheduled Tasks UI: create, edit, delete cron-based tasks with preset schedules, enable/disable toggle, manual Run Now, run history with output/error display, and human-readable cron descriptions. Instructions Management UI: list, view/edit, create (with task/queue templates), archive (two-tap), and queue status dropdown management. |
| 7 Apr 202612:30 am |
koda-core |
2.25.0 |
Scheduled task engine: node-cron scheduler with DB-backed tasks, slash command and shell script execution, run history logging, Signal notifications on failure, and full REST API. Instructions API: file CRUD, queue table parser, and status management endpoints. |
| 6 Apr 202611:30 pm |
koda-core |
2.24.0 |
Signal reply/quote support: extract quoted message context from inbound replies (prepended to Claude prompt), add outbound quote plumbing to signalSend/signalSendGroup for future reply-to capability. |
| 6 Apr 20268:35 pm |
koda-core |
2.23.2 |
Fix phantom message bug: Signal WebSocket double-delivers messages with different envelope shapes (sourceNumber vs sourceUuid). Multi-key dedup now registers all sender identifier variants so duplicates are caught regardless of format. |
| 6 Apr 20265:35 pm |
koda-core |
2.23.1 |
Fix session ID cross-contamination: tag inbound messages with sessionId at write time instead of greedy post-hoc backfill that stole messages from other sessions. |
| 6 Apr 20263:45 pm |
koda-core |
2.23.0 |
Rename koda_bridge → koda_core across code, DB role, JWT env var (BRIDGE_JWT → CORE_JWT), postgrest.conf, schema.sql, and docs. Live DB migrated via ALTER ROLE rename. |
| 3 Apr 20261:00 pm |
dashboard-generator |
1.7.0 |
Cross-site design overhaul: WCAG AA contrast fix (--text-dim 4.5:1+), 44px touch targets, focus-visible outlines, tap feedback, bumped font sizes, flow page mobile rewrite (version navigator, card-based history), landing page aria labels, changelog expandable rows with date/time stacking. |
| 3 Apr 202612:00 pm |
koda-core |
2.22.1 |
Fix message queue race condition: move processNext() call to server.js finally block after releaseGroup() to prevent re-queued messages seeing group as still busy. |
| 1 Apr 20269:54 pm |
koda-console |
1.14.0 |
Credentials section in More tab: list stored credentials (masked), add new, delete. No reveal in PWA — use /cred reveal via Signal. |
| 1 Apr 20269:54 pm |
koda-core |
2.22.0 |
/cred list command + /api/credentials endpoints (GET list, POST create, DELETE remove). Owner-only, audit-logged, passwords always masked in API responses. |
| 1 Apr 20269:28 pm |
koda-core |
2.21.0 |
/stop now sends SIGINT to running Claude sessions (Ctrl+C equivalent). Also fixes auto-continue self-messaging bug (#59), auto-archives completed work queues, and updates ack message wording. |
| 1 Apr 20263:55 pm |
koda-console |
1.13.0 |
Task edit converted to slide-up modal with full-size fields, labels editor, split save/cancel buttons (green Save), and safe-area-aware bottom padding. |
| 1 Apr 20263:05 pm |
koda-console |
1.12.0 |
Task UI polish: click-to-edit, task IDs shown, star priority pickers, auto-grow textareas, carry title to detailed form, styled Add button, swapped save/cancel. Chat: Signal banner dismiss-only. |
| 1 Apr 20261:45 pm |
koda-console |
1.11.0 |
Chat UX overhaul: session list as chat home with back arrow nav, click-to-view with lazy resume, pencil rename, active session highlight, Signal banner fixes with notification caveat. |
| 1 Apr 20261:45 pm |
koda-core |
2.20.0 |
Session history API returns live message counts and merges current active sessions with current flag. PWA and Signal sessions unified in one list. |
| 1 Apr 202612:00 pm |
koda-console |
1.9.0 |
Add live elapsed timers to dashboard widgets: 'Updated X ago' on Claude Usage, running duration on active jobs (dashboard + More tab). |
| 1 Apr 202611:30 am |
koda-console |
1.10.0 |
Drop into Signal DM from PWA: session history shows Signal/PWA badges, resume Signal sessions, transport-aware chat with Signal echo. |
| 1 Apr 202611:30 am |
koda-core |
2.19.0 |
Signal styled text mode on all responses, session history serves both Signal and PWA sessions, chat API transport param echoes responses to Signal DM. |
| 1 Apr 202610:00 am |
koda-core |
2.18.0 |
Fix session history: backfill claude_session_id on PWA chat messages, deduplicate archiveSession, stop resetting sessions on shutdown, clean up stale history entries. |
| 31 Mar 20268:58 pm |
deploy-watch |
1.1.0 |
Fetch Cloudflare API token from credential store via get-cred.js instead of inline .env decrypt. Retry loop for PostgREST availability at boot. |
| 31 Mar 20268:57 pm |
koda-core |
2.17.0 |
Fetch runtime secrets (Cloudflare API token, CF Access service token) from encrypted credential store at startup instead of .env. New get-cred.js utility for shell scripts. |
| 31 Mar 20266:00 pm |
koda-core |
2.16.0 |
Per-group message queuing with cancel support, message dedup at ingestion (60s TTL), 10min queue expiry, auto-process next queued message after session ends. Removed dead /webhook route. |
| 31 Mar 20269:02 am |
koda-console |
1.8.0 |
PWA chat batch: markdown rendering (marked.js + DOMPurify), streaming thinking blocks with collapsible UI, unread message badge on Chat tab, Enter for newline / Cmd+Enter to send, tighter bubble spacing, cross-device polling (30s). |
| 31 Mar 20269:02 am |
koda-core |
2.15.1 |
Add onThinking callback to Claude invocation for streaming thinking blocks via SSE to PWA chat. |
| 31 Mar 20267:55 am |
koda-console |
1.7.0 |
PWA nav batch: swap Chat/Tasks tabs, always-visible active jobs, task sorting, delete archived sessions, pull-to-refresh, hamburger popup menu, iOS zoom fix. |
| 31 Mar 20261:45 am |
koda-core |
2.15.0 |
Hot-reload infrastructure: lib modules can now be reloaded without restarting Koda Core. self-modify.sh tries /internal/hot-reload before falling back to restart. Includes auto-continue, /stop kill switch, and plan-aware notifications from autonomy-01. |
| 31 Mar 202612:15 am |
koda-core |
2.14.1 |
Fix usage scrape failing after restart by clearing stale Chromium SingletonLock before launching browser. |
| 30 Mar 20266:55 pm |
koda-console |
1.6.2 |
Chat UX: anchor input bar above tab bar, round send button with grey-when-empty to indigo-when-ready state change, filled send icon. |
| 30 Mar 20266:50 pm |
koda-console |
1.6.1 |
Fix iOS PWA: move safe-area padding to app shell so update/offline banners clear the notch. Prevent auto-zoom on input focus with maximum-scale=1. |
| 30 Mar 20266:45 pm |
koda-console |
1.6.0 |
Session history browser: slide-up drawer in Chat tab to view, rename, and resume past sessions. Active session tracking for context continuity across resume. |
| 30 Mar 20266:30 pm |
koda-core |
2.14.0 |
Session history & resume: archive sessions on reset, list/rename/resume API, auto-naming from first message, session_id filtering on chat history, claude_session_id tracking on messages. |
| 30 Mar 20266:15 pm |
koda-console |
1.5.1 |
Fix PWA update banner: remove skipWaiting() from SW install so new versions enter waiting state and trigger the update banner. Remove client.navigate() from activate — reload handled by controllerchange listener. |
| 30 Mar 20266:00 pm |
koda-console |
1.5.0 |
Console UX overhaul: 4-tab nav (Sessions+Settings moved into More), running jobs on dashboard, sticky Select button, scope pills hidden when only All/Global, cleaned up test data from groups and memories. |
| 30 Mar 202610:35 am |
koda-console |
1.4.1 |
Transitional SW bootstrap: restore skipWaiting() so existing mobile clients (without update banner) can receive the new SW with network-first HTML and update banner code. |
| 30 Mar 202610:26 am |
koda-console |
1.4.0 |
PWA update banner: 'New version available — tap to update' shown when a new service worker is waiting. Network-first strategy for HTML ensures fresh content. SW template read per-request (no restart needed for SW changes). |
| 30 Mar 202610:26 am |
koda-core |
2.13.2 |
Read sw.js template per-request instead of caching at startup — SW file changes no longer require Koda Core restart. |
| 30 Mar 20269:46 am |
koda-console |
1.3.0 |
Console UX improvements: pill-styled Select button, friendly session names (not UUIDs), consolidated single task list (removed 248-line duplicate), auto-versioned SW cache from versions.json. |
| 30 Mar 20269:46 am |
koda-core |
2.13.1 |
Sessions API returns displayName (group name or user DM label), /status sessions uses user lookup for DM labels, /console/sw.js route injects version-based cache name. |
| 29 Mar 202611:00 pm |
koda-core |
2.12.1 |
Fix duplicate Signal notifications during multi-step plans: suppress redundant self-modify.sh, startup/shutdown, and progress notifications. |
| 29 Mar 202610:30 pm |
koda-console |
1.2.0 |
Add Chat tab to Console PWA — real-time streaming conversation with Claude via SSE, message history, typing indicators, new chat/reset, 6-column tab bar. |
| 29 Mar 202610:30 pm |
koda-core |
2.13.0 |
Add /api/chat endpoints — SSE streaming chat (send/history/reset) with correct system prompt building, session continuity, and message persistence. |
| 29 Mar 20268:30 pm |
koda-console |
1.1.0 |
Add dedicated Tasks tab to Console PWA — scope filter pills, quick/full add forms, task cards with priority/labels/due dates, inline edit, multi-select with bulk done/delete, completed section. |
| 29 Mar 20268:30 pm |
koda-core |
2.12.0 |
Scoped task management: upgraded tasks DB schema (scope, priority, labels, due_date), /api/tasks REST endpoints (CRUD + bulk + work-on-it), enhanced /task command (scope-aware, flags, multi-ID done, edit, delete, instruction file generation via /task work). |
| 29 Mar 20266:25 pm |
koda-core |
2.11.0 |
Mount Koda Console PWA on Koda Core at /console/ (Tier 1 same-origin serving). Eliminates cross-origin CF Access cookie issues. |
| 29 Mar 20265:30 pm |
koda-core |
2.10.1 |
Fix CF_TEAM_DOMAIN fallback default from 'koda' to 'koda-systems' in middleware.js. Fix race condition in self-modify.sh where intentional-restart sentinel was created too late, causing 'interrupted' message on planned restarts. |
| 29 Mar 20262:30 pm |
koda-core |
2.10.0 |
NL intent pipeline: lib/intents.js parses koda-intent blocks from Claude responses, validates against app manifests, sends confirmation prompts via Signal, executes against app Worker APIs, audit logs to intent_log table. Yes/no intercept in message handler. |
| 29 Mar 20262:20 pm |
koda-core |
2.9.0 |
App registration + intent context: lib/apps.js registry module with manifest validation and in-memory cache, intent context injection into Claude system prompts, /api/apps/intents and /api/intents endpoints, intent_log DB table, DB conventions documentation, scaffold-app.sh updated with standard D1 tables and helpers. |
| 29 Mar 202612:15 pm |
koda-core |
2.8.0 |
Standalone app framework (Tier 2): /api/apps CRUD endpoints, koda_apps DB table, scaffold-app.sh and deploy-app.sh scripts for Cloudflare Workers + D1 apps. |
| 29 Mar 202611:30 am |
koda-console |
1.0.0 |
Initial release: Koda Console PWA deployed to console.koda.systems. Alpine.js + HTMX + Tailwind (no build step). 4-tab layout: Dashboard, Sessions, Settings, More. PWA installable on iPhone. Service worker shell caching. |
| 29 Mar 202611:00 am |
koda-core |
2.7.0 |
REST API for Console PWA: /api/* endpoints (auth, status, usage, sessions, model, route, memories, users, messages, groups, jobs, config). JWT auth via Cloudflare Access. DB migration: email column on users. |
| 29 Mar 202610:30 am |
koda-core |
2.6.0 |
Cloudflare Tunnel (api.koda.systems) + Access auth + CORS middleware for /api routes |
| 26 Mar 202610:45 am |
koda-core |
2.5.0 |
Fix attachment saving: fetch data from signal-cli REST API by attachment ID instead of expecting inline base64. Add long message support: detect text/x-signal-plain attachments and replace truncated message body with full text. |
| 26 Mar 20268:55 am |
koda-core |
2.4.0 |
Group messaging fixes: Signal group send format (group.base64), mention detection (U+FFFC + mentions array), group name sync on startup, friendly names in /status sessions, signal-notify.sh group format detection, /plan and /continue commands for multi-step plan continuation across restarts |
| 20 Mar 20267:00 pm |
koda-core |
2.3.0 |
Restart notifications with reason context (pre-restart, shutdown, startup messages include modified file), signal-notify.sh reusable wrapper, mandatory progress reporting instructions for multi-step plans via Signal |
| 20 Mar 20265:45 pm |
koda-core |
2.2.0 |
Group web publishing and research assistant features: /activate with group type registry, /pages, /research, /jobs commands, background task runner, multi-format document generation (PDF/PPTX/DOCX/MD), Signal file delivery, RBAC capabilities (web_publish/web_edit/web_delete/research), group-type-specific skill auto-loading, page-design-guide MCP integration |
| 20 Mar 202612:30 pm |
dashboard-generator |
1.6.0 |
Glassmorphism redesign for status page: glass cards with backdrop-filter, ambient background orbs, service pills, gradient hardware metrics, reduced-motion support |
| 20 Mar 202612:00 pm |
koda-core |
2.1.0 |
Rename service com.koda.claude-bridge → com.koda.core, exec fix for PID alignment, log files bridge-* → core-* |
| 19 Mar 202610:00 pm |
koda-core |
2.0.0 |
Modular decomposition: server.js split into 16 lib modules + 18 hot-swappable command handlers with fs.watch auto-reload |
| 19 Mar 20262:00 pm |
vscode-bridge |
0.2.4 |
Replace exec with execFile in SearchFilesTool, add command logging (audit-011) |
| 19 Mar 20261:30 pm |
deploy-watch |
1.0.1 |
Shell hardening and config cleanup (audit-010) |
| 19 Mar 20261:00 pm |
koda-core |
1.9.3 |
Remove unused dependencies crypto, body-parser, nodemailer (audit-009) |
| 19 Mar 202612:30 pm |
koda-core |
1.9.2 |
Replace fragile 15s wait with selector-based wait in usage scraper (audit-008) |
| 19 Mar 202612:00 pm |
koda-core |
1.9.1 |
Add input validation for slash command arguments (audit-007) |
| 19 Mar 202611:30 am |
koda-core |
1.9.0 |
Server resilience improvements — file permission health check, startup validation (audit-006) |
| 19 Mar 202610:30 am |
koda-core |
1.8.10 |
Mask credentials in /cred get, add /cred reveal command (audit-003) |
| 19 Mar 202610:00 am |
koda-core |
1.8.9 |
Require Authorization header on /db proxy endpoint (audit-002) |
| 19 Mar 20269:30 am |
dashboard-generator |
1.5.0 |
Upgrade auth from SHA-256 to PBKDF2, move hash to env var (audit-004) |
| 19 Mar 20265:00 am |
koda-core |
1.8.8 |
Version key rename signal-bridge → koda-core in versions.json |
| 19 Mar 20264:00 am |
koda-core |
1.8.7 |
Show route and model in 'working on that' acknowledgement message |
| 19 Mar 20263:00 am |
koda-core |
1.8.4 |
Enhance /reset reply with route, model, and usage info; AI-authored deploy confirmations |
| 19 Mar 20262:00 am |
koda-core |
1.8.3 |
Fix /model clear route label, enhance /model clear to show route and model info |
| 19 Mar 20261:30 am |
koda-core |
1.8.1 |
Fix version reading from koda-core key instead of signal-bridge (was undefined) |
| 19 Mar 20261:00 am |
koda-core |
1.8.0 |
Implement /route and /model dual-command system for Claude CLI and VS Code routing |
| 19 Mar 202612:00 am |
dashboard-generator |
1.4.0 |
Add Information Flow page (/flow) — SVG flowchart of Signal→Koda→Claude message path |
| 18 Mar 20269:00 pm |
koda-core |
1.7.5 |
Sentinel file approach: intentional restarts send 'restarting' instead of 'interrupted' |
| 18 Mar 20268:00 pm |
dashboard-generator |
1.3.1 |
Rename 'Bridge' → 'Koda Core' in service names, page title, and subtitle |
| 18 Mar 20267:00 pm |
koda-core |
1.7.4 |
Fix stripMarkdown mangling underscore-delimited identifiers (e.g. ENV_VAR_NAMES) |
| 18 Mar 20266:30 pm |
koda-core |
1.7.3 |
/status: show timeout value and elapsed time per active session; add deployment notification script |
| 18 Mar 20266:00 pm |
koda-core |
1.7.2 |
/status: remove auto-approve/approvals, unify service labels, show model and usage for active route |
| 18 Mar 20265:45 pm |
koda-core |
1.7.1 |
Deduplicate shutdown notifications when owner has an active session |
| 18 Mar 20265:30 pm |
dashboard-generator |
1.3.0 |
Add changelog page, version timestamps on status page, 60s auto-refresh via launchd cron |
| 18 Mar 20265:30 pm |
koda-core |
1.7.0 |
15-minute default timeout, suppress duplicate errors on shutdown, fix owner error notification to use UUID |
| 18 Mar 20266:15 am |
koda-core |
1.6.1 |
Use OWNER_UUID directly for all owner notifications instead of phone lookup |
| 18 Mar 20266:10 am |
koda-core |
1.6.0 |
Notify active sessions on bridge shutdown; track active sessions by group |
| 18 Mar 20265:30 am |
koda-core |
1.5.2 |
Friendlier error messages for Claude exit code 143/137 (SIGTERM/SIGKILL) |
| 18 Mar 20265:25 am |
koda-core |
1.5.1 |
Show bridge version in /status command output |
| 18 Mar 20265:20 am |
koda-core |
1.5.0 |
Auto-approve default on, /model display rework, two-phase /modify flow |
| 17 Mar 202610:00 pm |
vscode-bridge |
0.2.3 |
Fix filesystem write failures using direct local tool invocation |
| 17 Mar 20268:00 pm |
vscode-bridge |
0.2.2 |
Model deduplication in VS Code bridge |
| 17 Mar 20266:00 pm |
dashboard-generator |
1.2.0 |
Three-page dashboard (landing, build, status); mobile-first CSS; clickable sections |
| 17 Mar 20264:00 pm |
koda-core |
1.4.2 |
Graceful shutdown cleanup to prevent process hangs |
| 16 Mar 202612:00 pm |
deploy-watch |
1.0.0 |
Initial release — fswatch + Cloudflare Pages deploy pipeline |