Skip to content

feat(cloud): add operation ledgers and event contract - #5093

Merged
iscekic merged 61 commits into
mainfrom
audit-w3b-operation-ledgers-8e34
Aug 17, 2026
Merged

feat(cloud): add operation ledgers and event contract#5093
iscekic merged 61 commits into
mainfrom
audit-w3b-operation-ledgers-8e34

Conversation

@iscekic

@iscekic iscekic commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

User: The app now keeps cloud and remote session creation tied to one user intent, so safe retries do not create duplicate sessions.

Product manager: PR actions, security commands, and organization writes now deduplicate safe retries; terminal outcomes use a durable analytics outbox; mobile tracks only settled visible leaf routes.

Maintainer: Postgres operation ledgers provide admission, replay, lease takeover, reconciliation, terminal settlement, and atomic outbox insertion. The session-ingest Durable Object keeps remote create deduplication. Mobile passes stable intent keys and contains post-success UI failures.

Changes:

  • Added a shared operation-ledger contract with admission, replay, terminal states, and lease takeover.
  • Added a typed privacy-minimal analytics event map and event catalog.
  • Added the durable analytics outbox and authenticated dispatch cron.
  • Added retry-safe cloud and remote session creation with stable intent keys and reconciliation.
  • Added retry-safe pull-request mutations with provider reconciliation for ambiguous outcomes.
  • Added retry-safe Security Sync commands and organization member writes with audit and outbox outcomes.
  • Added settled visible leaf-route tracking with redirect and KiloClaw exclusions.

Human steps: None. The normal deployment migration process applies the generated PostgreSQL migration.

Visual Changes: N/A

E2E: bot-e2e — final iOS verification passed for cloud duplicate replay, analytics outbox delivery, organization role replay, cloud takeover reconciliation, settled Home tracking, and KiloClaw route exclusion.

Verification: pnpm test:db, root typecheck, root lint, changed-file formatting, shared analytics tests, cloud SDK tests, worker-utils tests, Cloud Agent Next tests, and cumulative implementation review passed. CodeQL remains red because repository code scanning is disabled; the same CodeQL jobs fail on main with HTTP 403 during SARIF upload.

@iscekic iscekic self-assigned this Aug 6, 2026
@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 6, 2026
Comment thread apps/web/src/routers/github-pr-review-router.ts Outdated
Comment thread apps/web/src/routers/organizations/organization-members-router.ts Outdated
Comment thread apps/web/src/routers/organizations/organization-members-router.ts Outdated
Comment thread apps/web/src/routers/organizations/organization-members-router.ts Outdated
Comment thread apps/web/src/routers/organizations/organization-members-router.ts Outdated
Comment thread apps/web/src/lib/security-agent/router/shared-handlers.ts Outdated
Comment thread apps/web/src/lib/security-agent/router/shared-handlers.ts Outdated
Comment thread apps/web/src/lib/security-agent/router/shared-handlers.ts
Comment thread services/cloud-agent-next/src/session/session-registration.ts Outdated
Comment thread apps/mobile/src/lib/hooks/use-organization-mutations.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

Incremental review of 1 new PR-authored commit (5b121f807, through HEAD) finds the carried poll-dependency WARNING is resolved — the security-sync Worker now settles the operation ledger itself at every terminal command transition and on retry exhaustion — but reveals one new WARNING: Worker-settled dismiss_finding events emit distinct_id as the raw user id because the dismiss actor schema carries no email, splitting the analytics identity against the email-channel used by every other settle path.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
services/security-sync/src/index.ts 507 NEW: Worker-settled dismiss_finding security_command_settled events use distinct_id = actor.idManualFindingDismissalCommandSchema.actor carries only id and web submits actor: { id: ctx.user.id } — while the web fallback and DEC-05 contract use the email identity channel, so a dismissal settled by the Worker lands under a different distinct_id than the user's other security events

SUGGESTION

File Line Issue
packages/cloud-agent-sdk/src/create-session.ts 91 Carried (unchanged file; re-verified last round): removing the bareFallbackProvenKeys cache makes every same-key retry against an old CLI pay a guaranteed wasted :ext round-trip before falling back to :bare
Resolved in this range
  • apps/web/src/lib/security-agent/router/shared-handlers.ts — terminal security-command settlement no longer depends solely on a client poll of getCommandStatus: the Worker settles the ledger row at both terminal transition points (sync + dismissal) via settleSecurityLedgerForTerminalCommand, retries on NOT-yet-recorded provider_ref through the queue retry loop, settles again on retry exhaustion (QUEUE_RETRIES_EXHAUSTED), and the poll remains as a CAS-only fallback — the durable security_command_settled event now fires even when the client never polls (prior WARNING fixed)
Files Reviewed (1 PR-authored commit)
  • 5b121f807 fix(security): settle command ledgers in worker — services/security-sync/src/index.ts, services/security-sync/src/index.test.ts, apps/web/src/lib/security-agent/router/shared-handlers.ts (doc comments) — 1 WARNING. Verified: settle CAS no-ops against terminal rows (worker vs web fallback emits the event exactly once), outbox properties validate against the security_command_settled strict schema (source: 'server', optional repo_count/error_count, non-negative integer duration_ms), the not-ready-row throw drives a queue retry that re-settles from the early-terminal branch without re-running the sync effect, and the exhaustion path settles before the terminal-outcome ack

Fix these issues in Kilo Cloud

@iscekic

iscekic commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

(bot) Kilobot review not detected after two retriggers, proceeding further

@iscekic

iscekic commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

(bot) Rejected the carried organization key warning because the hook owns one mounted mutation surface, and a remount starts a new user interaction. The key remains stable across retryable errors while that owner remains mounted; no new evidence changes the approved plan contract.

@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 7, 2026
Net -3080 lines with no functional change.

- Collapsed duplicated ledger helpers: one bounded canonical-result merge, one
  non-terminal row update, one outbox validation.
- Deleted a third dedupe layer in the cloud-agent SDK and a verbatim copy of the
  mobile spawn classifier.
- Collapsed repeated admit/settle/reconcile blocks into one helper per router.
- Merged the two identical security worker clients into one shared client.
- Deleted the unwired analytics capture module, the speculative route-lifecycle
  publisher, and the mobile analytics re-export hop.
- Reused existing helpers: cron auth, terminal-status predicate, organization
  role list, and the shared owner/repo schema.
- Cut doc comments that restated the code and removed duplicated test coverage.
Comment thread services/cloud-agent-next/src/session/session-registration.ts
Comment thread apps/web/src/routers/organizations/organization-members-router.ts Outdated
Comment thread packages/cloud-agent-sdk/src/create-session.ts
iscekic added 10 commits August 17, 2026 14:35
- Deleted `setOperationProviderRef`: dead, and the only ledger write path with
  no row lock and no terminal-status check.
- Deleted `resolvePrDistinctId`: `ctx.user` already holds the email, so the
  per-mutation user query is gone.
- The role-change repair path now audits the role as reconciled instead of
  claiming a `from` role it never observed.
- A scheduled security sync no longer reads the operation ledger; only a manual
  trigger can hold a row.
- The PR and organization routers import the shared settled-event constants
  instead of hardcoding the names.
- Finding dismissal has its own in-progress copy instead of the sync wording.
- Recorded the screens that mobile tracking stops reporting.
- Added a development-only warning naming redacted analytics keys.
- One `runAfterResponse` in `apps/web/src/lib/after-response.ts`. It keeps the
  hardened error handling: a synchronous `after()` throw and a rejected work
  promise are both reported instead of becoming unhandled rejections.
- One `sha256Hex` in `services/cloud-agent-next/src/utils/sha256.ts`.

`canonicalJson` stays duplicated on purpose. The two copies serialize
`undefined`, sparse arrays, `Date`, `NaN`, and `Infinity` differently, and the
output is a dedupe identity, so merging them would rotate every in-flight
operation key for the ledger's retention window.
`UQ_organization_memberships_org_user` already makes the role update and the
membership delete idempotent, and the gateway-state revoke is idempotent too.
The ledger only added a 120-second lease that rejected a legitimate second
attempt, so it is gone along with the `operationKey` input, the admission
dispatcher, the repair and replay paths, and the mobile key hoisting.

Removed `organization_write_settled`. It was unconsumed and outbox-only, so
keeping it would have needed a new capture path.

Net -2327 lines. Authorization, owner authority, every error message, the audit
rows, and the post-removal cleanup are unchanged. Three tests that only the
deleted ledger suite covered moved into the router test.

Accepted trade-off: a duplicate tap now writes a second audit row.
The field list lived twice, once in the router and once per mobile hook. Drift
would have rotated every in-flight operation key and rejected same-key retries
for the ledger's 30-day retention window.

`packages/app-shared/src/pr-review/intent-fingerprint.ts` now owns the field
list and builds the fingerprint; the router and the three mobile hooks import
it. The router keeps the `owner/repo#number` prefix and the hashing, because
`node:crypto` is not platform-agnostic.

Output is byte-identical over 44 inputs across the four intents, covering absent
optionals, reversed key-insertion order, unicode, and quoting. Both old copies
agreed on every input. Pinned-literal tests now guard both sides.
`security_agent_commands` already holds the terminal state the UI polls, and the
dismissal is idempotent against the GitHub alert state, so retry safety needs
only a key on the command row.

`operation_key` plus two partial unique indexes, one per owner column, now
enforce it. The owner must be in the identity because a key is unique per actor
only; a single composite index cannot work, since the unused owner column is
NULL and Postgres treats NULLs as distinct.

The worker loses its ledger reads, its settle calls, its outbox insertion, and
the queue-send claim fence: net -485 lines. A keyed enqueue creates or finds the
command row and sends no queue message when the row already exists. The web side
settles its own ledger row when it observes a terminal command, so only the
ledger helpers still insert outbox rows.

Deploy `apps/web` first, then `services/security-sync`.
Security, share, and session hooks imported `useHoistedOperationKey` from
`lib/pr-review/merge/pr-operation-ledger.ts`, which is the wrong dependency
direction. The intent-agnostic pieces now live in `lib/operation-key.ts`: the
hook, both server markers, and one shared `isOperationInProgress` predicate that
replaces the two near-identical copies.

The PR module keeps its PR-only messages, surface copy, and error mapping. Each
surface still supplies its own in-progress copy, so the PR, security sync, and
dismissal strings stay distinct.
The PR added `eslint-disable max-lines` to two files instead of splitting them.
Both disables are gone.

- `use-continue-session.ts` gives up its cloud-agent leg to
  `use-continue-cloud-create.ts`: 291 and 103 lines, was 369.
- `use-security-findings.ts` gives up the remediation write hooks to
  `use-security-remediation.ts`: 203 and 186 lines, was 379.

The post-success containment moved verbatim: the key still rotates before the
UI work, each step is contained on its own, and only a non-retryable
`prepareSession` error ends the intent.
Four conflicts, all in apps/mobile. Both sides' intent survives in each.

- `use-pr-review-mutations.ts`, `use-pr-merge-mutations.ts`: kept the ledger
  `mutationFn` and main's accessibility announcements. Failures now announce
  through `announcingToast` carrying the mapped ledger copy.
- `use-new-session-creator.ts`: kept main's `onCreated` draft clear inside the
  post-success containment, and contained the analytics and cache-invalidation
  calls together so neither can skip it.
- `use-new-session-creator.test.ts` was added on both sides. Combined into one
  harness with all 20 tests, 10 per side.

`use-pr-review-mutations.test.ts` needed main's `react-native` mock, since the
production file now reaches `announce.ts` and the pure vitest project cannot
parse Flow source.
A definitively-lost `createSessionWithInitialAdmission` RPC left the operation
key stuck: the ownership row existed, both metadata reads stayed absent, and
every same-key retry threw `creation_in_progress` forever, so the client never
reached a terminal outcome.

Branch (c) now settles the row `failed` with `create_rpc_abandoned` once it
outlives `SESSION_CREATE_ABANDON_AFTER_SECONDS` (900s, 7.5x the create lease).
Below that age the behaviour is unchanged.

The double-execution guard is intact: nothing is deleted, no fresh create is
allocated from absent state, and the settle only flips the row terminal. A later
same-key retry replays the terminal outcome, and the user's next attempt carries
a new key, new session IDs, and a new Durable Object.

`BAD_REQUEST session_creation_failed` already classifies as terminal on mobile,
so the client rotates its key instead of retrying.

The test fixture now dates rows relative to now. Its hard-coded date made every
row 11 days old against the real clock, which would have put the existing
branch-(c) tests into the abandonment path.
…_ref

This PR added two migrations. Neither has been applied to production, so they
are now one: `0216_familiar_boomer`.

`IDX_operation_ledgers_provider_ref` is now `WHERE provider_ref IS NOT NULL`.
The column is NULL for every non-security domain, so the partial form is
smaller and equally fast for the join it serves.

Anyone who already ran `0216_secret_alex_wilder` or `0217_public_kylun`
locally must reset their local database.
Comment thread apps/web/src/lib/security-agent/router/shared-handlers.ts
Comment thread services/security-sync/src/index.ts
Comment thread services/security-sync/src/index.ts Outdated
Comment thread apps/web/src/routers/github-pr-review-router.ts
Comment thread apps/web/src/lib/analytics-outbox/dispatch.ts Outdated
Comment thread apps/mobile/src/components/agents/use-new-session-creator.ts Outdated
Comment thread packages/db/src/migrations/0216_familiar_boomer.sql Outdated
Comment thread packages/app-shared/src/analytics/privacy.ts
Comment thread docs/analytics-event-catalog.md Outdated
@kilo-code-bot

kilo-code-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of the 17 files changed since the previous review: all 7 prior findings (security-sync same-key re-enqueue, ambiguous outbox event identity, PII in dispatch logs, uncontained onCreated, non-concurrent unique indexes, DEC-05 deny-list gaps, stale doc references) are verified fixed, and no new issues were found in the changed code.

Files Reviewed (17 files)
  • services/security-sync/src/index.ts + test - same-key retry now resets QUEUE_ADMISSION_FAILED commands via CAS transition and re-enqueues; race-safe, failure loop consistent
  • packages/db/src/operation-ledger.ts - ambiguous phase gets a distinct :ambiguous outbox identity, so the later terminal event is no longer dropped; centralized fix covers all callers
  • apps/web/src/lib/analytics-outbox/dispatch.ts + test - distinct_id removed from Sentry-bound log fields; remaining use is the PostHog payload only
  • apps/mobile/src/components/agents/use-new-session-creator.ts + test - onCreated contained in try/catch; navigation can no longer be skipped by a host callback throw
  • packages/db/src/migrations/0216_motionless_triathlon.sql, meta/0216_snapshot.json, meta/_journal.json, packages/db/src/schema.ts - unique indexes now CONCURRENTLY with the COMMIT/BEGIN wrapper matching repo precedent (0115/0118/0140)
  • packages/app-shared/src/analytics/privacy.ts + event-map.test.ts - deny-list adds password/passwd/auth/key/credential; the one affected catalog key (auth_ready) is explicitly carved out; app_startup keys unaffected
  • packages/db/src/security-agent-command-repository.ts - non-terminal transitions now clear completed_at, keeping the retried command's timestamps coherent
  • apps/web/src/routers/github-pr-review-router.ts - comment updated to match the new outbox identity semantics
  • docs/analytics-event-catalog.md - references now point to existing code (trackKiloPassPurchaseCompleted, appsflyer.ts trackEvent, uncataloged captureEvent overload)
  • apps/web/src/lib/analytics-outbox/operation-ledger.integration.test.ts - expectation updated to one event per phase (ambiguous + terminal)

No memory leaks introduced (no new listeners, intervals, or unbounded structures).

Previous Review Summary (commit 4cdebb1)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 4cdebb1)

Status: 7 Issues Found | Recommendation: Address before merge

Executive Summary

The security-sync same-key dedupe branch acknowledges retries of already-terminally-failed commands with 202 accepted and never re-enqueues them — silently dropping the user action under exactly the transient failure this PR's retry-safety work targets; a second systemic issue drops the definitive terminal analytics event once a row has gone through the ambiguous path.

Overview

Severity Count
CRITICAL 1
WARNING 5
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
services/security-sync/src/index.ts 288 Same-key retry returns 202 accepted for a command already terminal failed (QUEUE_ADMISSION_FAILED) without re-enqueueing — the effect never runs and every retry repeats the false success

WARNING

File Line Issue
apps/web/src/routers/github-pr-review-router.ts 853 Ambiguous pr_operation_settled outbox event consumes the deterministic (row, eventName) identity, so the later confirmed terminal settle event is silently dropped (same defect for security_command_settled in shared-handlers.ts)
apps/web/src/lib/analytics-outbox/dispatch.ts 129 outboxLogFields logs distinct_id (the user's email) into Sentry-bound warning/error logs, against the repo's PII-redaction posture
apps/mobile/src/components/agents/use-new-session-creator.ts 148 Uncontained onCreated?.() can skip post-success navigation and be swallowed silently, leaving a rotated key and re-enabled button — a second tap creates a duplicate cloud session
packages/db/src/migrations/0216_familiar_boomer.sql 41 Non-concurrent CREATE UNIQUE INDEX on existing, populated security_agent_commands (2 statements) blocks writes during the build; repo precedent uses CONCURRENTLY for populated tables
packages/app-shared/src/analytics/privacy.ts 15 DEC-05 deny-list misses credential classes (password, api_key, auth_header, …) — the runtime redactor, sole guard on the uncataloged AppsFlyer mirror path, lets them through unredacted

SUGGESTION

File Line Issue
docs/analytics-event-catalog.md 15 Authoritative doc references non-existent code: apps/web/src/lib/analytics-outbox/capture.ts, captureCatalogEvent, captureUncataloged
Files Reviewed (106 files)
  • services/security-sync/src/index.ts - 1 issue
  • apps/web/src/routers/github-pr-review-router.ts - 1 issue
  • apps/web/src/lib/security-agent/router/shared-handlers.ts - covered by router finding
  • apps/web/src/lib/analytics-outbox/dispatch.ts - 1 issue
  • apps/mobile/src/components/agents/use-new-session-creator.ts - 1 issue
  • packages/db/src/migrations/0216_familiar_boomer.sql - 1 issue
  • packages/app-shared/src/analytics/privacy.ts - 1 issue
  • docs/analytics-event-catalog.md - 1 issue
  • packages/db/src/operation-ledger.ts, analytics-outbox.ts, security-agent-command-repository.ts, schema.ts, index.ts, package.json - ledger admission/settle/CAS, outbox atomicity, partial indexes verified clean
  • apps/web/src/app/api/cron/dispatch-analytics-outbox/route.ts + dispatch.ts loop - cron auth, bounded claim batching, claim fencing, dead-letter handling verified clean
  • apps/web/src/lib/security-agent/** (services, schemas), routers/organizations/organization-members-router.ts, lib/user/index.ts (PII deletion coverage), vercel.json - verified clean
  • packages/app-shared/src/analytics/event-map.ts, pr-review/intent-fingerprint.ts, packages/cloud-agent-sdk/*, packages/worker-utils/src/cloud-agent-next-client.ts - verified clean
  • services/cloud-agent-next/** (session-registration reconcile ladder, intent fingerprints, sha256 util, backup cache), services/session-ingest/* - verified clean
  • apps/mobile/** (operation-key rotation/dedupe hooks, security-agent mutations, screen tracking w/ listener/interval cleanup, share gate, PR-review mutations) - verified clean; no memory leaks found (no unbounded maps, intervals, or un-removed listeners introduced)
  • plus associated test files across all pages

Note: existing-comment reconciliation was unavailable during this run (GitHub returned 403 on comment-list endpoints), so duplicates against pre-existing human comments could not be excluded.

Fix these issues in Kilo Cloud


Reviewed by kimi-k3 · Input: 157K · Output: 11.7K · Cached: 524.2K

Review guidance: REVIEW.md from base branch main

@iscekic
iscekic enabled auto-merge (squash) August 17, 2026 16:39
@iscekic
iscekic merged commit a8af778 into main Aug 17, 2026
60 of 67 checks passed
@iscekic
iscekic deleted the audit-w3b-operation-ledgers-8e34 branch August 17, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants