Skip to content

fix(sessions): link a pull request only on the session's verified own evidence - #6968

Merged
iscekic merged 7 commits into
mainfrom
kwf/owner-pr-session-link-cloud-20260929
Oct 4, 2026
Merged

iscekic merged 7 commits into
mainfrom
kwf/owner-pr-session-link-cloud-20260929

Conversation

@iscekic

@iscekic iscekic commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Changelog for users

  • Sessions show only the pull request they opened; a reused branch name no longer surfaces another session's PR.
  • A session without a verified link shows no PR badge instead of an unrelated one.
  • A verified link whose live state has not synced shows a pending card with a working Refresh action.
  • A reported PR URL that names a different repository is dropped and no link is stored.

Changelog for maintainers

  • The (git_url, git_branch, tenant) branch fallback join is removed; the session-to-PR join now keys on the session's own stored link.
  • pr_link_verified_at is a new nullable column left null on pre-existing links, so stale links never render; no backfill.
  • session_pr_link and the heartbeat prLink accept optional headRef and headSha, stored as pr_head_ref and pr_head_sha.
  • Only a session whose own head evidence matches gets pr_state and pr_review_decision written; the cache row is keyed by repo and PR number.
  • Start with the link-identity helper and the session-to-PR join predicate; then check the refresh path sets and revokes verification.
  • Commit verification walks at most MAX_COMMIT_PAGES pages; a session SHA outside that window is refused rather than paged for.
  • A concurrent index on (git_url, pr_number) replaces branch-keyed lookups for webhook and review-decision updates.
  • The cache re-key is staged inside one migration, 0269: it adds the session columns and nulls pr_number on older (git_url, pr_number, owner) duplicates, builds the new indexes concurrently, then drops the branch-key indexes. A unique index exists at every step. Accepted deploy window: production runs migrations before promotion, so from the 0269 index drop until promotion the old revision has no branch-key conflict target. Its webhook cache upserts are caught and skipped, and its refresh calls return an error. The next webhook or Refresh after promotion writes the PR-identity row.
  • Webhook verification applies the same branch rule as refresh, and requires the stored pr_url to name the webhook PR.
  • The review-decision platform allowlist is deleted, so a session on any platform can flag a pending review.

E2E proof

A session with no session-reported, GitHub-verified link shows no PR at all: the (git_url, git_branch, tenant) branch-name fallback in formatAssociatedPr and sessionPrJoinPredicate is removed and unverified links render no associatedPr.

Asserted value: kwf session-pr-link read path: PASS. Sense check (model): Head log shows 'kwf session-pr-link read path: PASS' with all 3 tests passing, while the base run failed 2 of 3 (toBeNull failure) showing the unverified-link defect.

Base log: backend-assert 32d9c35b6bdc exited 1
$ set -euo pipefail
$ cd apps/web
$ CFG=kwf-jest.config.cjs
$ TEST=src/routers/kwf-pr-link-read-path.test.ts
$ cat > "$CFG" <<'JESTCFG'
$ module.exports = {
$   rootDir: __dirname,
$   testEnvironment: 'node',
$   testMatch: ['<rootDir>/src/routers/kwf-pr-link-read-path.test.ts'],
$   globalSetup: '<rootDir>/src/tests/setup/globalSetup.ts',
$   transform: {
$     '^.+[.](t|j)sx?$': [
$ … 84 more line(s)
      at Object.toBeNull (src/routers/kwf-pr-link-read-path.test.ts:39:66)
Test Suites: 1 failed, 1 total
Tests:       2 failed, 1 passed, 3 total
Snapshots:   0 total
Time:        3.933 s
Ran all test suites.
kwf session-pr-link read path jest rc=1
kwf session-pr-link read path: FAIL
Head log: backend-assert 32d9c35b6bdc exited 0
$ set -euo pipefail
$ cd apps/web
$ CFG=kwf-jest.config.cjs
$ TEST=src/routers/kwf-pr-link-read-path.test.ts
$ cat > "$CFG" <<'JESTCFG'
$ module.exports = {
$   rootDir: __dirname,
$   testEnvironment: 'node',
$   testMatch: ['<rootDir>/src/routers/kwf-pr-link-read-path.test.ts'],
$   globalSetup: '<rootDir>/src/tests/setup/globalSetup.ts',
$   transform: {
$     '^.+[.](t|j)sx?$': [
$ … 84 more line(s)
    ✓ a verified link that matches the cache still shows the PR (1 ms)
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        4.243 s
Ran all test suites.
kwf session-pr-link read path jest rc=0
kwf session-pr-link read path: PASS

A verified link whose per-PR cache row has not synced still renders the session's own pending partial (state unknown, no borrowed cache fields), keeping the Refresh action reachable after a missed webhook.

Asserted value: kwf pr-pending: PASS. Sense check (model): head ui-assert 0a6632ff8fb3 log shows 'kwf pr-pending: PASS' and the head screenshot shows the Agents session list with the E2E PR pending row and 'pull request 1', matching the base run's passing preserved behaviour

Base da3426980f96 Head 24b828eb17ab
Base after drive: the Android Agents Session history list with the E2E PR pending row showing a pending pull request 1 mark and no live state Head after drive: the Android Agents Session history list with the E2E PR pending row showing a pending pull request 1 mark and no live state

After drive: the Android Agents Session history list with the E2E PR pending row showing a pending pull request 1 mark and no live state

Base log: ui-assert 0a6632ff8fb3 exited 0
$ set -uo pipefail
$ out=$(node "$KWF_EXPLORER_SIM_CLI" do check --literal 'pull request 1') && rc=0 || rc=$?
$ echo "kwf pr-pending pull-request-1 rc=$rc: $out"
$ if [ "$rc" -ne 0 ]; then echo "kwf pr-pending: FAIL - the verified pending partial is not shown"; exit 1; fi
$ echo "kwf pr-pending: PASS"
kwf pr-pending pull-request-1 rc=0: shown
kwf pr-pending: PASS
shown: pull request 1 (via script, p=1.00, 17 elements, read 2178 ms, total 2180 ms): Button "E2E PR pending, fix/pending-ffa242af8c27, pull request 1, PR-PENDING-FFA242AF8C27, 1 minute ago, and from…
Head log: ui-assert 0a6632ff8fb3 exited 0
$ set -uo pipefail
$ out=$(node "$KWF_EXPLORER_SIM_CLI" do check --literal 'pull request 1') && rc=0 || rc=$?
$ echo "kwf pr-pending pull-request-1 rc=$rc: $out"
$ if [ "$rc" -ne 0 ]; then echo "kwf pr-pending: FAIL - the verified pending partial is not shown"; exit 1; fi
$ echo "kwf pr-pending: PASS"
kwf pr-pending pull-request-1 rc=0: shown
kwf pr-pending: PASS
shown: pull request 1 (via script, p=1.00, 17 elements, read 2163 ms, total 2165 ms): Button "E2E PR pending, fix/pending-eb31ab056065, pull request 1, PR-PENDING-EB31AB056065, 1 minute ago, and from…

A link is accepted only when GitHub confirms the PR base repository equals the PR head repository equals the session repository (normalized host+owner+name, case-insensitive), the PR head ref equals the session branch, and the link is the session's own stored link; a fork, another repo with the same branch name, or a reused branch name never links.

Asserted value: kwf pr-link-identity: PASS. Sense check (model): Head log names the asserted value with 'kwf pr-link-identity: PASS' after 3 passed tests, and no base run is present so the head alone is judged.

The scripts were proven on an earlier base, so only the head ran.

Head log: backend-assert f000af80efb9 exited 0
$ set -euo pipefail
$ cd apps/web
$ TEST=src/lib/integrations/platforms/github/kwf-pr-link-identity.test.ts
$ cat > "$TEST" <<'JESTTEST'
$ import { describe, expect, it } from '@jest/globals';
$ type Verify = (args: unknown) => { verified: boolean; reason?: string };
$ async function loadVerify(): Promise<Verify> {
$   const mod = (await import('@/lib/integrations/platforms/github/pr-link-identity')) as Record<string, unknown>;
$   const fn = mod.verifySessionPullRequestLink as Verify | undefined;
$   expect(typeof fn).toBe('function');
$   return fn as Verify;
$ }
$ … 30 more line(s)
    ✓ rejects a PR whose URL names another repository with the same branch name (2 ms)
    ✓ accepts the session own repo, head repo, head ref and head sha (1 ms)
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        4.778 s, estimated 5 s
Ran all test suites within paths "src/lib/integrations/platforms/github/kwf-pr-link-identity.test.ts".
kwf pr-link-identity: PASS

Existing branch-fallback links disappear after deploy without a backfill: pr_link_verified_at is a nullable column left null on pre-existing links so unverified and stale links are never surfaced.

Asserted value: KWF_TEMP pr_link_verified_at added nullable: true. Sense check (model): Head log line 'KWF_TEMP pr_link_verified_at added nullable: true' names the asserted value and head exits 0, while the base test exited 1 on the migration assertion.

Base log: backend-assert 10d21dc58eae exited 1
$ set -euo pipefail
$ cd packages/db
$ cat > src/kwf-pr-link-verified-nullable.test.ts <<'JESTTEST'
$ import { describe, expect, it } from '@jest/globals';
$ import * as fs from 'fs';
$ import * as path from 'path';
$ const dir = path.join(__dirname, 'migrations');
$ function readTag(tag: string): string {
$   const journal = JSON.parse(fs.readFileSync(path.join(dir, 'meta', '_journal.json'), 'utf8')) as { entries: Array<{ tag: string }> };
$   const entry = journal.entries.find(e => e.tag.endsWith(tag));
$   if (!entry) throw new Error('journal is missing ' + tag);
$   return fs.readFileSync(path.join(dir, entry.tag + '.sql'), 'utf8');
$ … 23 more line(s)
    expect(received).toBeGreaterThan(expected)
    Expected: > 0
      20 |     const backfill = /update\s+"?cli_sessions_v2"?[^;]*pr_link_verified_at/i.test(sqlText);
      at Object.toBeGreaterThan (src/kwf-pr-link-verified-nullable.test.ts:17:26)
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.223 s
Ran all test suites matching src/kwf-pr-link-verified-nullable.test.ts.
kwf pr-link-verified-nullable: FAIL
Head log: backend-assert 10d21dc58eae exited 0
$ set -euo pipefail
$ cd packages/db
$ cat > src/kwf-pr-link-verified-nullable.test.ts <<'JESTTEST'
$ import { describe, expect, it } from '@jest/globals';
$ import * as fs from 'fs';
$ import * as path from 'path';
$ const dir = path.join(__dirname, 'migrations');
$ function readTag(tag: string): string {
$   const journal = JSON.parse(fs.readFileSync(path.join(dir, 'meta', '_journal.json'), 'utf8')) as { entries: Array<{ tag: string }> };
$   const entry = journal.entries.find(e => e.tag.endsWith(tag));
$   if (!entry) throw new Error('journal is missing ' + tag);
$   return fs.readFileSync(path.join(dir, entry.tag + '.sql'), 'utf8');
$ … 23 more line(s)
    KWF_TEMP pr_link_verified_at added nullable: true
  KWF pr-link identity migration
    ✓ adds pr_link_verified_at nullable and never backfills it (23 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.209 s
Ran all test suites matching src/kwf-pr-link-verified-nullable.test.ts.
kwf pr-link-verified-nullable: PASS

The pull_request/pull_request_review verified-link gate has an exact (git_url, pr_number) index (IDX_cli_sessions_v2_git_url_pr_number, built concurrently) so webhook cost no longer scales with sessions per repository.

Asserted value: KWF_TEMP index IDX_cli_sessions_v2_git_url_pr_number: concurrent true. Sense check (jev): probability 0.92

Base log: backend-assert e824410dde82 exited 1
$ set -euo pipefail
$ cd packages/db
$ cat > src/kwf-pr-gate-index.test.ts <<'JESTTEST'
$ import { describe, expect, it } from '@jest/globals';
$ import * as fs from 'fs';
$ import * as path from 'path';
$ const dir = path.join(__dirname, 'migrations');
$ function readTag(tag: string): string {
$   const journal = JSON.parse(fs.readFileSync(path.join(dir, 'meta', '_journal.json'), 'utf8')) as { entries: Array<{ tag: string }> };
$   const entry = journal.entries.find(e => e.tag.endsWith(tag));
$   if (!entry) throw new Error('journal is missing ' + tag);
$   return fs.readFileSync(path.join(dir, entry.tag + '.sql'), 'utf8');
$ … 22 more line(s)
    expect(received).toBeGreaterThan(expected)
    Expected: > 0
      20 |     const columns = /\("git_url","pr_number"\)/.test(sqlText);
      at Object.toBeGreaterThan (src/kwf-pr-gate-index.test.ts:17:26)
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.223 s
Ran all test suites matching src/kwf-pr-gate-index.test.ts.
kwf pr-gate-index: FAIL
Head log: backend-assert e824410dde82 exited 0
$ set -euo pipefail
$ cd packages/db
$ cat > src/kwf-pr-gate-index.test.ts <<'JESTTEST'
$ import { describe, expect, it } from '@jest/globals';
$ import * as fs from 'fs';
$ import * as path from 'path';
$ const dir = path.join(__dirname, 'migrations');
$ function readTag(tag: string): string {
$   const journal = JSON.parse(fs.readFileSync(path.join(dir, 'meta', '_journal.json'), 'utf8')) as { entries: Array<{ tag: string }> };
$   const entry = journal.entries.find(e => e.tag.endsWith(tag));
$   if (!entry) throw new Error('journal is missing ' + tag);
$   return fs.readFileSync(path.join(dir, entry.tag + '.sql'), 'utf8');
$ … 22 more line(s)
    KWF_TEMP index IDX_cli_sessions_v2_git_url_pr_number: concurrent true columns true
  KWF cli_sessions_v2 PR gate index
    ✓ builds IDX_cli_sessions_v2_git_url_pr_number concurrently over (git_url, pr_number) (21 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.204 s
Ran all test suites matching src/kwf-pr-gate-index.test.ts.
kwf pr-gate-index: PASS

Session list, search, getWithRuntimeState, worktreeDetails and active-sessions still attach the session's own associatedPr with live state, title and review decision for a verified link.

Asserted value: KWF_TEMP list associatedPr number: 7. Sense check (jev): probability 0.91

Base log: backend-assert 9240b31de16e exited 0
$ set -euo pipefail
$ cd apps/web
$ TEST=src/routers/kwf-list-pr-link.test.ts
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, kilocode_users } from '@kilocode/db/schema';
$ import { eq, sql } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import { createCallerForUser } from '@/routers/test-utils';
$ jest.mock('@/lib/cloud-agent-next/cloud-agent-client', () => ({ createCloudAgentNextClient: jest.fn(() => ({ getSession: jest.fn().mockRejectedValue(new Error('not mocked')), deleteSession: jest.fn()…
$ jest.mock('@/lib/auth/resource-delegation', () => ({ createControlTokenForRequest: jest.fn(async () => ({ token: 'test-token' })) }));
$ … 44 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP list associatedPr number: 7 state=open title=Live PR review=approved
PASS src/routers/kwf-list-pr-link.test.ts (11.775 s)
  ✓ list attaches the session own verified PR with live fields (35 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        11.831 s
Ran all test suites within paths "src/routers/kwf-list-pr-link.test.ts".
kwf list-pr-link: PASS
Head log: backend-assert 9240b31de16e exited 0
$ set -euo pipefail
$ cd apps/web
$ TEST=src/routers/kwf-list-pr-link.test.ts
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, kilocode_users } from '@kilocode/db/schema';
$ import { eq, sql } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import { createCallerForUser } from '@/routers/test-utils';
$ jest.mock('@/lib/cloud-agent-next/cloud-agent-client', () => ({ createCloudAgentNextClient: jest.fn(() => ({ getSession: jest.fn().mockRejectedValue(new Error('not mocked')), deleteSession: jest.fn()…
$ jest.mock('@/lib/auth/resource-delegation', () => ({ createControlTokenForRequest: jest.fn(async () => ({ token: 'test-token' })) }));
$ … 44 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP list associatedPr number: 7 state=open title=Live PR review=approved
PASS src/routers/kwf-list-pr-link.test.ts (11.793 s)
  ✓ list attaches the session own verified PR with live fields (39 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        11.848 s
Ran all test suites within paths "src/routers/kwf-list-pr-link.test.ts".
kwf list-pr-link: PASS

PR title and PR number search still return sessions for their linked pull request through the shared session-to-PR join.

Asserted value: KWF_TEMP search pr-title hits: 1. Sense check (jev): probability 0.92

Base log: backend-assert d5e8abc5a04e exited 0
$ set -euo pipefail
$ cd apps/web
$ TEST=src/routers/kwf-search-pr-link.test.ts
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, kilocode_users } from '@kilocode/db/schema';
$ import { eq, sql } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import { createCallerForUser } from '@/routers/test-utils';
$ jest.mock('@/lib/cloud-agent-next/cloud-agent-client', () => ({ createCloudAgentNextClient: jest.fn(() => ({ getSession: jest.fn().mockRejectedValue(new Error('not mocked')), deleteSession: jest.fn()…
$ jest.mock('@/lib/auth/resource-delegation', () => ({ createControlTokenForRequest: jest.fn(async () => ({ token: 'test-token' })) }));
$ … 48 more line(s)
    KWF_TEMP search pr-title hits: 1
PASS src/routers/kwf-search-pr-link.test.ts (11.6 s)
  ✓ search returns the session by linked PR title and by PR number (43 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        11.66 s
Ran all test suites within paths "src/routers/kwf-search-pr-link.test.ts".
kwf search-pr-link: PASS
Head log: backend-assert d5e8abc5a04e exited 0
$ set -euo pipefail
$ cd apps/web
$ TEST=src/routers/kwf-search-pr-link.test.ts
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, kilocode_users } from '@kilocode/db/schema';
$ import { eq, sql } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import { createCallerForUser } from '@/routers/test-utils';
$ jest.mock('@/lib/cloud-agent-next/cloud-agent-client', () => ({ createCloudAgentNextClient: jest.fn(() => ({ getSession: jest.fn().mockRejectedValue(new Error('not mocked')), deleteSession: jest.fn()…
$ jest.mock('@/lib/auth/resource-delegation', () => ({ createControlTokenForRequest: jest.fn(async () => ({ token: 'test-token' })) }));
$ … 48 more line(s)
    KWF_TEMP search pr-title hits: 1
PASS src/routers/kwf-search-pr-link.test.ts (10.628 s)
  ✓ search returns the session by linked PR title and by PR number (37 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        10.683 s
Ran all test suites within paths "src/routers/kwf-search-pr-link.test.ts".
kwf search-pr-link: PASS

The pull_request and pull_request_review webhooks still maintain pr_state and pr_review_decision in the state cache for pull requests a session verifiably links.

Asserted value: KWF_TEMP webhook review_decision_pending: true. Sense check (jev): probability 0.96

Base log: backend-assert f499bc8455cd exited 0
$ set -euo pipefail
$ cd apps/web
$ TEST=src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-state.test.ts
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, kilocode_users } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import { upsertCliSessionPullRequestsFromWebhook } from '@/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-requests';
$ import { upsertCliSessionPullRequestReviewFromWebhook } from '@/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-request-review';
$ const NS = createHash('sha256').update(process.env.KWF_SCENARIO_NAMESPACE ?? 'kwf-state').digest('hex').slice(0, 12);
$ … 52 more line(s)
    KWF_TEMP webhook review_decision_pending: true
PASS src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-state.test.ts
  ✓ maintains pr_state then review_decision_pending for the linked PR (39 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.834 s
Ran all test suites within paths "src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-state.test.ts".
kwf pr-state: PASS
Head log: backend-assert f499bc8455cd exited 0
$ set -euo pipefail
$ cd apps/web
$ TEST=src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-state.test.ts
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, kilocode_users } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import { upsertCliSessionPullRequestsFromWebhook } from '@/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-requests';
$ import { upsertCliSessionPullRequestReviewFromWebhook } from '@/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-request-review';
$ const NS = createHash('sha256').update(process.env.KWF_SCENARIO_NAMESPACE ?? 'kwf-state').digest('hex').slice(0, 12);
$ … 52 more line(s)
    KWF_TEMP webhook review_decision_pending: true
PASS src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-state.test.ts
  ✓ maintains pr_state then review_decision_pending for the linked PR (63 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.877 s
Ran all test suites within paths "src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-state.test.ts".
kwf pr-state: PASS

session_pr_link accepts optional headRef and headSha and stores them as cli_sessions_v2.pr_head_ref/pr_head_sha; when headSha is present the link is shown only if GitHub reports that SHA as the PR head or as one of the PR's commits, and when absent verification falls back to repo + head repo + head ref, never branch-only.

Asserted value: KWF_TEMP headRef stored: fix/typo. Sense check (jev): probability 0.93

Base log: backend-assert 7c7e7859eb26 exited 1
$ cd services/session-ingest
$ rm -f /tmp/kwf-s1.marker
$ cat > src/ingest/kwf-session-pr-link-ingest.test.ts <<'EOF'
$ import { describe, expect, it, vi } from 'vitest';
$ import * as fs from 'node:fs';
$ vi.mock('cloudflare:workers', () => ({
$   DurableObject: class {
$     ctx: unknown;
$     env: unknown;
$     constructor(ctx: unknown, env: unknown) {
$       this.ctx = ctx;
$       this.env = env;
$ … 44 more line(s)
 ❯ src/ingest/kwf-session-pr-link-ingest.test.ts (3 tests | 3 failed) 15ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/ingest/kwf-session-pr-link-ingest.test.ts > KWF session PR link ingest evidence > stores the head ref and head sha the session reported
AssertionError: expected { prPlatform: 'github', …(2) } to match object { prPlatform: 'github', …(4) }
- Expected
 FAIL  src/ingest/kwf-session-pr-link-ingest.test.ts > KWF session PR link ingest evidence > clears the stored verification whenever a link or head field changes
AssertionError: expected undefined to be 'fix/typo' // Object.is equality
- Expected:
 FAIL  src/ingest/kwf-session-pr-link-ingest.test.ts > KWF session PR link ingest evidence > rejects a PR URL that names another repository
AssertionError: expected 'undefined' to be 'function' // Object.is equality
Expected: "function"
     49|     if (typeof fn !== 'function') return;
     50|     expect(fn('https://github.com/kilo/other/pull/2', 'https://github.…
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯
 Test Files  1 failed (1)
      Tests  3 failed (3)
   Start at  15:32:57
   Duration  1.86s (transform 726ms, setup 0ms, import 1.69s, tests 15ms, environment 0ms)
KWF_TEMP headRef stored: undefined
Head log: backend-assert 7c7e7859eb26 exited 0
$ cd services/session-ingest
$ rm -f /tmp/kwf-s1.marker
$ cat > src/ingest/kwf-session-pr-link-ingest.test.ts <<'EOF'
$ import { describe, expect, it, vi } from 'vitest';
$ import * as fs from 'node:fs';
$ vi.mock('cloudflare:workers', () => ({
$   DurableObject: class {
$     ctx: unknown;
$     env: unknown;
$     constructor(ctx: unknown, env: unknown) {
$       this.ctx = ctx;
$       this.env = env;
$ … 44 more line(s)
 RUN  v4.1.11 /tmp/kwf-script-88ccfec9acd230839f245ef2af8856e8cee2ee3177ce56303e7e2be1ad1db6cf/services/session-ingest
 ✓ src/ingest/kwf-session-pr-link-ingest.test.ts (3 tests) 6ms
 Test Files  1 passed (1)
      Tests  3 passed (3)
   Start at  15:33:03
   Duration  1.98s (transform 799ms, setup 0ms, import 1.82s, tests 6ms, environment 0ms)
KWF_TEMP headRef stored: fix/typo

Ingest rejects a session_pr_link or heartbeat prLink whose PR URL owner/repo does not match the session's git_url, storing nothing rather than a guess.

Asserted value: KWF_TEMP foreign rejected: false. Sense check (model): Base log FAILs 'expected undefined to be function' (gate missing), head log passes and prints 'KWF_TEMP foreign rejected: false', the asserted value.

Base log: backend-assert a4ace03d8c05 exited 1
$ set -euo pipefail
$ cd services/session-ingest
$ cat > src/ingest/kwf-pr-url-gate.test.ts <<'EOF'
$ import { describe, expect, it, vi } from 'vitest';
$ import * as fs from 'node:fs';
$ vi.mock('cloudflare:workers', () => ({
$   DurableObject: class {
$     ctx: unknown;
$     env: unknown;
$     constructor(ctx: unknown, env: unknown) {
$       this.ctx = ctx;
$       this.env = env;
$ … 33 more line(s)
 ❯ src/ingest/kwf-pr-url-gate.test.ts (2 tests | 2 failed) 1755ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/ingest/kwf-pr-url-gate.test.ts > KWF ingest PR-link repo gate > rejects a PR URL that names another repository and accepts a matching one
AssertionError: expected 'undefined' to be 'function' // Object.is equality
Expected: "function"
 FAIL  src/ingest/kwf-pr-url-gate.test.ts > KWF ingest PR-link repo gate > wires the same gate into the metadata write and the heartbeat drop
AssertionError: expected 'import { and, eq, inArray, isNotNull,…' to contain 'prUrlMatchesGitUrl'
- Expected
+     } catch (error) {
+       console.error('Failed to invalidate session access after organization scope change', {
+         error: error instanceof Error ? error.message : String(error),
+     console.error('Failed to flush partial metadata changes after ingest error', {
+       error: err instanceof Error ? err.message : String(err),
       |                         ^
     37|     expect(connectionSrc).toContain('prUrlMatchesGitUrl');
     38|   });
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
 Test Files  1 failed (1)
      Tests  2 failed (2)
   Start at  15:33:14
   Duration  1.97s (transform 787ms, setup 0ms, import 53ms, tests 1.76s, environment 0ms)
Head log: backend-assert a4ace03d8c05 exited 0
$ set -euo pipefail
$ cd services/session-ingest
$ cat > src/ingest/kwf-pr-url-gate.test.ts <<'EOF'
$ import { describe, expect, it, vi } from 'vitest';
$ import * as fs from 'node:fs';
$ vi.mock('cloudflare:workers', () => ({
$   DurableObject: class {
$     ctx: unknown;
$     env: unknown;
$     constructor(ctx: unknown, env: unknown) {
$       this.ctx = ctx;
$       this.env = env;
$ … 33 more line(s)
 RUN  v4.1.11 /tmp/kwf-script-a249d98d92da2aadcfdca82984d600e213319890e8bd793e852233305d5edfd4/services/session-ingest
 ✓ src/ingest/kwf-pr-url-gate.test.ts (2 tests) 1742ms
     ✓ rejects a PR URL that names another repository and accepts a matching one  1739ms
 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  15:33:23
   Duration  1.94s (transform 782ms, setup 0ms, import 50ms, tests 1.74s, environment 0ms)
KWF_TEMP foreign rejected: false

Per-session evidence replaces per-tenant/per-branch matching: the pull_request webhook marks a session verified and writes the state cache only for a session that stored the same PR number on the same repo and whose head evidence matches, and github_branch_pull_requests is keyed and conflict-updated by (git_url, pr_number, tenant).

Asserted value: KWF_TEMP webhook no-link cache rows: 0 rc=0. Sense check (jev): probability 0.91

Base log: backend-assert 7c6e1c22fc86 exited 1
$ set -euo pipefail
$ cd apps/web
$ TEST=src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-cache-identity.test.ts
$ LOG="$(mktemp)"
$ OUT="$(mktemp)"
$ rm -f "$TEST"
$ trap 'rm -f "$TEST" "$LOG" "$OUT"' EXIT
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ … 47 more line(s)
kwf pr-cache-identity: FAIL
    expect(received).toHaveLength(expected)
    Expected length: 0
    expect(received).toHaveLength(expected)
    Expected length: 2
      49 | });
      50 |
      at Object.toHaveLength (src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-cache-identity.test.ts:48:16)
Test Suites: 1 failed, 1 total
Tests:       2 failed, 2 total
Snapshots:   0 total
Time:        4.079 s
Ran all test suites within paths "src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-cache-identity.test.ts".
Head log: backend-assert 7c6e1c22fc86 exited 0
$ set -euo pipefail
$ cd apps/web
$ TEST=src/lib/integrations/platforms/github/webhook-handlers/kwf-pr-cache-identity.test.ts
$ LOG="$(mktemp)"
$ OUT="$(mktemp)"
$ rm -f "$TEST"
$ trap 'rm -f "$TEST" "$LOG" "$OUT"' EXIT
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ … 47 more line(s)
KWF_TEMP webhook no-link cache rows: 0 rc=0
KWF_TEMP webhook shared-branch rows: 2 prs=5,6
kwf pr-cache-identity: PASS

refreshAssociatedPullRequest verifies the stored link against GitHub (base/head repo, head ref, head SHA or commit list) before writing the cache and sets pr_link_verified_at only on success, revoking it on mismatch; the cache is written by PR identity.

Asserted value: KWF_TEMP refresh foreign-repo associatedPr: null githubCalls=0. Sense check (model): Head log line "KWF_TEMP refresh foreign-repo associatedPr: null githubCalls=0" names the asserted value and exits 0, while the base run exits 1 with "Expected number of calls: 0" failing, showing the defect.

Base log: backend-assert cc6e5dc71215 exited 1
$ set -euo pipefail
$ cd apps/web
$ TEST=src/routers/kwf-refresh-pr-link.test.ts
$ LOG="$(mktemp)"
$ OUT="$(mktemp)"
$ rm -f "$TEST"
$ trap 'rm -f "$TEST" "$LOG" "$OUT"' EXIT
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, platform_integrations } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ … 57 more line(s)
kwf refresh-pr-link: FAIL
FAIL src/routers/kwf-refresh-pr-link.test.ts (10.92 s)
    Expected number of calls: 0
      59 | });
      60 |
      at Object.toBeNull (src/routers/kwf-refresh-pr-link.test.ts:57:28)
Test Suites: 1 failed, 1 total
Tests:       2 failed, 2 total
Snapshots:   0 total
Time:        10.978 s
Ran all test suites within paths "src/routers/kwf-refresh-pr-link.test.ts".
Head log: backend-assert cc6e5dc71215 exited 0
$ set -euo pipefail
$ cd apps/web
$ TEST=src/routers/kwf-refresh-pr-link.test.ts
$ LOG="$(mktemp)"
$ OUT="$(mktemp)"
$ rm -f "$TEST"
$ trap 'rm -f "$TEST" "$LOG" "$OUT"' EXIT
$ cat > "$TEST" <<'JESTTEST'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, platform_integrations } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { createHash } from 'node:crypto';
$ … 57 more line(s)
KWF_TEMP refresh foreign-repo associatedPr: null githubCalls=0
KWF_TEMP refresh fork associatedPr: null cacheRows=0
kwf refresh-pr-link: PASS

Branch-name fallback removed: a session whose own link is not GitHub-verified (pr_link_verified_at null) yields associatedPr null with no cache-by-branch path (cli-sessions-v2-router.ts:431-460), and active-sessions enrichment gates on verified_at so a branch-only session omits associatedPr (apps/web/src/lib/active-sessions-list.ts:186-196,432-447).

Asserted value: KWF_TEMP C5 linkless associatedPr: null KWF_TEMP C5 unverified associatedPr: null. Sense check (jev): probability 0.9

Base log: backend-assert f5b22d25e335 exited 1
$ cd apps/web
$ cat > src/routers/kwf-c5-branch-fallback.test.ts <<'EOF'
$ import { formatAssociatedPr } from '@/routers/cli-sessions-v2-router';
$ const CACHE = {
$   pr_url: 'https://github.com/kilo/repo/pull/42',
$   pr_number: 42,
$   pr_state: 'open',
$   pr_title: 'Add feature X',
$   pr_head_sha: 'deadbeef',
$   pr_last_synced_at: '2026-01-01T00:00:00.000Z',
$   pr_review_decision: 'approved',
$   review_decision_pending: false,
$ … 35 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
FAIL src/routers/kwf-c5-branch-fallback.test.ts
Test Suites: 1 failed, 1 total
Tests:       2 failed, 2 total
Snapshots:   0 total
Time:        4.747 s
Ran all test suites matching src/routers/kwf-c5-branch-fallback.test.ts.
    KWF_TEMP C5 linkless associatedPr: {"url":"https://github.com/kilo/repo/pull/42","number":42,"state":"open","title":"Add feature X","headSha":"deadbeef","lastSyncedAt":"2026-01-01T00:00:00.000Z",…
    KWF_TEMP C5 unverified associatedPr: {"url":"https://github.com/kilo/repo/pull/42","number":42,"state":"open","title":"Add feature X","headSha":"deadbeef","lastSyncedAt":"2026-01-01T00:00:00.000Z…
      25 |     console.log('KWF_TEMP C5 linkless associatedPr: ' + JSON.stringify(result));
      39 |     console.log('KWF_TEMP C5 unverified associatedPr: ' + JSON.stringify(result));
Head log: backend-assert f5b22d25e335 exited 0
$ cd apps/web
$ cat > src/routers/kwf-c5-branch-fallback.test.ts <<'EOF'
$ import { formatAssociatedPr } from '@/routers/cli-sessions-v2-router';
$ const CACHE = {
$   pr_url: 'https://github.com/kilo/repo/pull/42',
$   pr_number: 42,
$   pr_state: 'open',
$   pr_title: 'Add feature X',
$   pr_head_sha: 'deadbeef',
$   pr_last_synced_at: '2026-01-01T00:00:00.000Z',
$   pr_review_decision: 'approved',
$   review_decision_pending: false,
$ … 35 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP C5 linkless associatedPr: null
    KWF_TEMP C5 unverified associatedPr: null
    ✓ shows no PR for a stored link GitHub has not verified (1 ms)
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.628 s
Ran all test suites matching src/routers/kwf-c5-branch-fallback.test.ts.
    KWF_TEMP C5 linkless associatedPr: null
    KWF_TEMP C5 unverified associatedPr: null

Session-to-PR cache join keys on PR identity (git_url, pr_number, tenant) and requires pr_url + pr_link_verified_at non-null, never (git_url, git_branch) (cli-sessions-v2-router.ts:508-539); cache unique indexes are (git_url, pr_number, owner) and the read index is (git_url, pr_number) (packages/db/src/schema.ts:6834-6850, migrations 0268/0269).

Asserted value: KWF_TEMP C6 identity join: true branch join: false verified gate: true. Sense check (jev): probability 0.94

Base log: backend-assert da87cf2f92ff exited 1
$ cd apps/web
$ cat > src/routers/kwf-c6-join.test.ts <<'EOF'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests } from '@kilocode/db/schema';
$ import { sessionPrJoinPredicate } from '@/routers/cli-sessions-v2-router';
$ describe('KWF C6 cache join key', () => {
$   it('joins the PR cache by PR identity and requires a verified link', () => {
$     const rendered = db
$       .select({ session_id: cli_sessions_v2.session_id })
$       .from(cli_sessions_v2)
$       .leftJoin(github_branch_pull_requests, sessionPrJoinPredicate)
$       .toSQL();
$ … 15 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
FAIL src/routers/kwf-c6-join.test.ts
    expect(received).toBe(expected) // Object.is equality
    Expected: true
      at Object.toBe (src/routers/kwf-c6-join.test.ts:16:26)
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.564 s
Ran all test suites matching src/routers/kwf-c6-join.test.ts.
    KWF_TEMP C6 identity join: false branch join: true verified gate: false
      15 |     console.log('KWF_TEMP C6 identity join: ' + identityJoin + ' branch join: ' + branchJoin + ' verified gate: ' + verifiedGate);
Head log: backend-assert da87cf2f92ff exited 0
$ cd apps/web
$ cat > src/routers/kwf-c6-join.test.ts <<'EOF'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests } from '@kilocode/db/schema';
$ import { sessionPrJoinPredicate } from '@/routers/cli-sessions-v2-router';
$ describe('KWF C6 cache join key', () => {
$   it('joins the PR cache by PR identity and requires a verified link', () => {
$     const rendered = db
$       .select({ session_id: cli_sessions_v2.session_id })
$       .from(cli_sessions_v2)
$       .leftJoin(github_branch_pull_requests, sessionPrJoinPredicate)
$       .toSQL();
$ … 15 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP C6 identity join: true branch join: false verified gate: true
  KWF C6 cache join key
    ✓ joins the PR cache by PR identity and requires a verified link (5 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.701 s
Ran all test suites matching src/routers/kwf-c6-join.test.ts.
    KWF_TEMP C6 identity join: true branch join: false verified gate: true

A link is shown only when GitHub confirms the stored link names the session repo, the PR base repo equals it, the PR head repo equals it (a fork or other repo with the same branch is rejected), and the PR head ref equals the session branch/head ref; only then is pr_link_verified_at set or the cache written (pr-link-identity.ts:251-323; cli-sessions-v2-router.ts:1959-1995).

Asserted value: KWF_TEMP C7 module loaded: true KWF_TEMP C7 own verified: true. Sense check (model): Head backend-assert 0c0b6cebebbd exited 0 and its output names the asserted value: 'KWF_TEMP C7 module loaded: true' and 'KWF_TEMP C7 own verified: true', with fork/other-repo/no-link rejected.

The scripts were proven on an earlier base, so only the head ran.

Head log: backend-assert 0c0b6cebebbd exited 0
$ cd services/session-ingest
$ cat > src/ingest/kwf-c7-identity.test.ts <<'EOF'
$ import { describe, expect, it } from 'vitest';
$ describe('KWF C7 strict session PR link identity', () => {
$   it('accepts only the session own PR and rejects every false link', async () => {
$     const mod = await import('../../../../apps/web/src/lib/integrations/platforms/github/pr-link-identity').catch(() => null);
$     console.log('KWF_TEMP C7 module loaded: ' + (mod !== null));
$     expect(mod).not.toBeNull();
$     if (!mod) return;
$     const sessionRepo = { gitUrl: 'https://github.com/kilo/repo.git', gitBranch: 'fix/typo' };
$     const link = { prUrl: 'https://github.com/kilo/repo/pull/7', prNumber: 7, headRef: 'fix/typo', headSha: 'sha-own' };
$     const own = mod.verifySessionPullRequestLink({ sessionRepo, link, pullRequest: { number: 7, baseRepoFullName: 'kilo/repo', headRepoFullName: 'kilo/repo', headRef: 'fix/typo', headSha: 'sha-own' }…
$ … 22 more line(s)
KWF_TEMP C7 module loaded: true
KWF_TEMP C7 own verified: true
   Start at  00:25:00
   Duration  180ms (transform 34ms, setup 0ms, import 33ms, tests 19ms, environment 0ms)
KWF_TEMP C7 module loaded: true
KWF_TEMP C7 own verified: true
KWF_TEMP C7 other repo verified: false
KWF_TEMP C7 fork verified: false
KWF_TEMP C7 legacy CLI verified: true
KWF_TEMP C7 no-link verified: false

session_pr_link accepts optional headRef/headSha (session-sync.ts:94-99) and persists them (session-ingest-extractors.ts:78-127, ingest/metadata.ts:114-118); with a head SHA the link verifies only if GitHub reports that SHA as the PR head or one of the PR's commits, and without it only repo+head-repo+head-ref checks apply, never branch-only (pr-link-identity.ts:304-322).

Asserted value: KWF_TEMP C8 headRef stored: fix/typo KWF_TEMP C8 headRef persisted: fix/typo. Sense check (model): Head output line 'KWF_TEMP C8 headRef persisted: fix/typo' names the asserted value and head backend-assert 3d3c8e161701 exited 0 on the head-alone change request.

The scripts were proven on an earlier base, so only the head ran.

Head log: backend-assert 3d3c8e161701 exited 0
$ cd services/session-ingest
$ cat > src/ingest/kwf-c8-head-evidence.test.ts <<'EOF'
$ import { describe, expect, it, vi } from 'vitest';
$ vi.mock('cloudflare:workers', () => ({ DurableObject: class { constructor() {} } }));
$ vi.mock('@kilocode/db/client', () => ({ getWorkerDb: vi.fn() }));
$ vi.mock('../dos/SessionAccessCacheDO', () => ({ getSessionAccessCacheDO: vi.fn() }));
$ vi.mock('../session-events', () => ({ mapSessionEventRow: vi.fn(), notifyUserSessionEvent: vi.fn() }));
$ import { extractSessionPrLink } from '../dos/session-ingest-extractors';
$ import { computeSessionMetadataUpdates } from './metadata';
$ describe('KWF C8 session_pr_link head evidence', () => {
$   it('stores and persists the reported head ref and sha', () => {
$     const result = extractSessionPrLink({ type: 'session_pr_link', data: { platform: 'github', prUrl: 'https://github.com/kilo/repo/pull/7', prNumber: 7, headRef: 'fix/typo', headSha: 'sha-own' } } a…
$ … 17 more line(s)
KWF_TEMP C8 headRef stored: fix/typo
KWF_TEMP C8 headRef persisted: fix/typo
 ✓ src/ingest/kwf-c8-head-evidence.test.ts (1 test) 5ms
 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  00:25:03
   Duration  1.61s (transform 656ms, setup 0ms, import 1.47s, tests 5ms, environment 0ms)
KWF_TEMP C8 headRef stored: fix/typo
KWF_TEMP C8 headRef persisted: fix/typo

Heartbeat prLink whose URL repo does not match the session git_url is dropped before it reaches the attachment or sessions.list (services/session-ingest/src/dos/UserConnectionDO.ts:663-676).

Asserted value: KWF_TEMP C10 heartbeat prLink: false active prLink: false. Sense check (jev): probability 0.92

The scripts were proven on an earlier base, so only the head ran.

Head log: backend-assert f9a5500bb5e8 exited 0
$ cd services/session-ingest
$ awk '/^describe[(]UserConnectionDO/{exit} {print}' src/dos/UserConnectionDO.test.ts > src/dos/kwf-c10-heartbeat-drop.test.ts
$ cat >> src/dos/kwf-c10-heartbeat-drop.test.ts <<'EOF'
$ describe('KWF C10 heartbeat PR link repo gate', () => {
$   it('drops a heartbeat prLink whose URL names another repo', async () => {
$     const { doInstance, mockCtx } = setup();
$     const cliWs = addCliSocket(mockCtx, 'cli-1');
$     const webWs = addWebSocket(mockCtx, 'web-1');
$     sendHeartbeat(doInstance, cliWs, [
$       {
$         id: 's1',
$         status: 'busy',
$ … 21 more line(s)
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > glanceable aggregate transitions > keeps heartbeat state and acknowledgement when aggregate transport fails
stderr | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > glanceable aggregate transitions > keeps heartbeat state and acknowledgement when aggregate transport fails
Glanceable aggregate refresh failed (non-fatal) { error: 'transport unavailable' }
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > heartbeat processing > fails an in-flight command when the session owner changes
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > stale connection eviction > closes stale connection after timeout
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > stale connection eviction > closes stale connection after timeout
Closing stale CLI connection (heartbeat timeout) { connectionId: 'cli-1' }
Closing stale CLI connection (heartbeat timeout) { connectionId: 'stale-1' }
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > CLI disconnect > sends error responses for pending commands on disconnect
stderr | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > CLI disconnect > sends error responses for pending commands on disconnect
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > CLI disconnect > sends error responses for pending commands on disconnect
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > CLI disconnect > fails pending commands as soon as their target socket is replaced
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > CLI disconnect > fails pending commands as soon as their target socket is replaced
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > CLI disconnect > sends error for connection-routed pending commands on CLI disconnect
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > CLI disconnect > sends error for connection-routed pending commands on CLI disconnect
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > UserConnectionDO > CLI disconnect > sends error for fallback-routed pending commands on CLI disconnect
stdout | src/dos/kwf-c10-heartbeat-drop.test.ts > KWF C10 heartbeat PR link repo gate > drops a heartbeat prLink whose URL names another repo
KWF_TEMP C10 heartbeat prLink: false active prLink: false
 ✓ src/dos/kwf-c10-heartbeat-drop.test.ts (300 tests) 1477ms
 Test Files  1 passed (1)
      Tests  300 passed (300)
   Start at  00:25:07
   Duration  3.85s (transform 1.24s, setup 0ms, import 2.25s, tests 1.48s, environment 0ms)
KWF_TEMP C10 heartbeat prLink: false active prLink: false

Existing branch-fallback links disappear after deploy without a data migration: reads require pr_link_verified_at IS NOT NULL (null on all pre-migration rows), and 0268 adds the columns and re-keys the cache indexes (packages/db/src/migrations/0268_pr_link_identity.sql).

Asserted value: KWF_TEMP C12 verified column: true identity unique: true branch unique: false. Sense check (model): head log line 'KWF_TEMP C12 verified column: true identity unique: true branch unique: false' names the asserted value and the head assert exited 0

The scripts were proven on an earlier base, so only the head ran.

Head log: backend-assert 4df3255a3617 exited 0
$ cd apps/web
$ cat > src/routers/kwf-c12-migration.test.ts <<'EOF'
$ import { getTableConfig } from 'drizzle-orm/pg-core';
$ import { cli_sessions_v2, github_branch_pull_requests } from '@kilocode/db/schema';
$ function columnsOf(index: { config: { columns: Array<{ name: string }> } }): string[] {
$   return index.config.columns.map(column => column.name);
$ }
$ describe('KWF C12 pr-link identity schema', () => {
$   it('adds the verified-link column and re-keys the cache unique index to PR identity', () => {
$     const sessionColumns = getTableConfig(cli_sessions_v2).columns.map(column => column.name);
$     const hasVerifiedColumn = sessionColumns.includes('pr_link_verified_at');
$     const indexes = getTableConfig(github_branch_pull_requests).indexes;
$ … 20 more line(s)
    KWF_TEMP C12 verified column: true identity unique: true branch unique: false
  KWF C12 pr-link identity schema
    ✓ adds the verified-link column and re-keys the cache unique index to PR identity (15 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5.626 s
Ran all test suites matching src/routers/kwf-c12-migration.test.ts.
    KWF_TEMP C12 verified column: true identity unique: true branch unique: false

A pull_request webhook writes the state/review cache only when a session of the tenant verifiably links that PR number+repo through its own head evidence (upsert-cli-session-pull-requests.ts:99-141,202-220); a PR opened by someone else on a reused branch name writes nothing.

Asserted value: KWF_TEMP C5 gate: markSessionsVerifyingPullRequest KWF_TEMP C5 gate result: no_session KWF_TEMP C5 marked sessions: 0 KWF_TEMP C5 cache rows written: 0. Sense check (model): head assert a83f2ac0fe4e exited 0 with log 'KWF_TEMP C5 gate result: no_session' and 'KWF_TEMP C5 cache rows written: 0', while the base run exited 1 showing 'KWF_TEMP C5 gate result: session' and 'cache rows written: 1'.

Base log: backend-assert a83f2ac0fe4e exited 1
$ cd apps/web
$ cat > src/lib/integrations/platforms/github/webhook-handlers/kwf-c5-gate.test.ts <<'EOF'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2 } from '@kilocode/db/schema';
$ import { eq, sql } from 'drizzle-orm';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import type { PullRequestPayload } from '@/lib/integrations/platforms/github/webhook-schemas';
$ const REPO = 'acme/widgets';
$ const GIT = 'https://github.com/' + REPO;
$ function makePayload(): PullRequestPayload {
$   return {
$     action: 'opened',
$ … 82 more line(s)
FAIL src/lib/integrations/platforms/github/webhook-handlers/kwf-c5-gate.test.ts
    expect(received).toBe(expected) // Object.is equality
    Expected: "no_session"
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        3.564 s
Ran all test suites matching src/lib/integrations/platforms/github/webhook-handlers/kwf-c5-gate.test.ts.
    KWF_TEMP C5 gate: checkMatchingSession
    KWF_TEMP C5 gate result: session
    KWF_TEMP C5 cache rows written: 1
      82 |       console.log('KWF_TEMP C5 cache rows written: ' + written);
Head log: backend-assert a83f2ac0fe4e exited 0
$ cd apps/web
$ cat > src/lib/integrations/platforms/github/webhook-handlers/kwf-c5-gate.test.ts <<'EOF'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2 } from '@kilocode/db/schema';
$ import { eq, sql } from 'drizzle-orm';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import type { PullRequestPayload } from '@/lib/integrations/platforms/github/webhook-schemas';
$ const REPO = 'acme/widgets';
$ const GIT = 'https://github.com/' + REPO;
$ function makePayload(): PullRequestPayload {
$   return {
$     action: 'opened',
$ … 82 more line(s)
    KWF_TEMP C5 gate: markSessionsVerifyingPullRequest
    KWF_TEMP C5 gate result: no_session
    KWF_TEMP C5 marked sessions: 0
    KWF_TEMP C5 cache rows written: 0
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.389 s
Ran all test suites matching src/lib/integrations/platforms/github/webhook-handlers/kwf-c5-gate.test.ts.
    KWF_TEMP C5 gate: markSessionsVerifyingPullRequest
    KWF_TEMP C5 gate result: no_session
    KWF_TEMP C5 marked sessions: 0
    KWF_TEMP C5 cache rows written: 0

A session's own verified PR still shows the badge with live PR state (open/closed/merged/draft), title, and review decision from the PR-identity cache row (formatCacheRow, cli-sessions-v2-router.ts:336-363,448-460).

Asserted value: own row PR number #1: 1. Sense check (model): Both base and head ui-assert logs print 'own row PR number #1: 1' with exit 0, and both screenshots show the E2E PR happy row with its own 'fix/typo · #1' badge, title 'E2E PR happy', and 'DISCUSSION-MIXED' review decision.

Base 30327537bbe3 Head 709af90c57fc
Base after ui-assert: the mobile Agents stored-session history list: the E2E PR happy row shows its own verified pull request 1 badge Head after ui-assert: the mobile Agents stored-session history list: the E2E PR happy row shows its own verified pull request 1 badge

After ui-assert: the mobile Agents stored-session history list: the E2E PR happy row shows its own verified pull request 1 badge

Base log: ui-assert 63b90b757dd6 exited 0
$ set -uo pipefail
$ CLI="$KWF_EXPLORER_SIM_CLI"
$ own=$(node "$CLI" do bounds 'fix/typo · #1' 2>/dev/null | grep -c '^bounds' || true)
$ echo "own row PR number #1: $own"
$ [ "$own" -ge 1 ] || { echo 'the session own verified PR number was not shown on its row'; exit 1; }
own row PR number #1: 1
Head log: ui-assert 63b90b757dd6 exited 0
$ set -uo pipefail
$ CLI="$KWF_EXPLORER_SIM_CLI"
$ own=$(node "$CLI" do bounds 'fix/typo · #1' 2>/dev/null | grep -c '^bounds' || true)
$ echo "own row PR number #1: $own"
$ [ "$own" -ge 1 ] || { echo 'the session own verified PR number was not shown on its row'; exit 1; }
own row PR number #1: 1

A session with no linked PR still renders with no PR badge (empty state) on the web session list/detail, the mobile Agents list, and the extension side panel.

Asserted value: linkless row shows a PR: 0. Sense check (model): head ui-assert b0b80d446f87 exited 0 with 'linkless row shows a PR: 0', matching base, and the screenshots show the E2E PR empty row with no PR badge

Base 30327537bbe3 Head 709af90c57fc
Base after ui-assert: the mobile Agents stored-session history list: the E2E PR empty row shows no PR badge Head after ui-assert: the mobile Agents stored-session history list: the E2E PR empty row shows no PR badge

After ui-assert: the mobile Agents stored-session history list: the E2E PR empty row shows no PR badge

Base log: ui-assert b0b80d446f87 exited 0
$ set -uo pipefail
$ CLI="$KWF_EXPLORER_SIM_CLI"
$ row=$(node "$CLI" do bounds 'E2E PR empty' 2>/dev/null | grep -c '^bounds' || true)
$ pr=$(node "$CLI" do check --literal 'pull request' 2>/dev/null | tr -d '\n') || true
$ hasPr=0; case "$pr" in 'not shown'*) hasPr=0;; *) hasPr=1;; esac
$ echo "linkless session row shown: $row"
$ echo "linkless row shows a PR: $hasPr"
$ [ "$row" -ge 1 ] || { echo 'the linkless session row was not shown'; exit 1; }
$ [ "$hasPr" = "0" ] || { echo 'a PR badge was shown for a session with no link'; exit 1; }
linkless session row shown: 1
linkless row shows a PR: 0
Head log: ui-assert b0b80d446f87 exited 0
$ set -uo pipefail
$ CLI="$KWF_EXPLORER_SIM_CLI"
$ row=$(node "$CLI" do bounds 'E2E PR empty' 2>/dev/null | grep -c '^bounds' || true)
$ pr=$(node "$CLI" do check --literal 'pull request' 2>/dev/null | tr -d '\n') || true
$ hasPr=0; case "$pr" in 'not shown'*) hasPr=0;; *) hasPr=1;; esac
$ echo "linkless session row shown: $row"
$ echo "linkless row shows a PR: $hasPr"
$ [ "$row" -ge 1 ] || { echo 'the linkless session row was not shown'; exit 1; }
$ [ "$hasPr" = "0" ] || { echo 'a PR badge was shown for a session with no link'; exit 1; }
linkless session row shown: 1
linkless row shows a PR: 0

The explicit Refresh PR info action still fetches the session's own PR and review decision from GitHub to recover a missed webhook (refreshAssociatedPullRequest, cli-sessions-v2-router.ts:1761-2098).

Asserted value: KWF_TEMP C13 refresh number: 7 KWF_TEMP C13 refresh state: open. Sense check (jev): probability 0.93

Base log: backend-assert 3b0b1ecf6221 exited 0
$ cd apps/web
$ cat > src/routers/kwf-c13-refresh.test.ts <<'EOF'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, platform_integrations } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import { createCallerForUser } from '@/routers/test-utils';
$ import * as githubAdapter from '@/lib/integrations/platforms/github/adapter';
$ jest.mock('@/lib/integrations/platforms/github/adapter', () => {
$   const actual: Record<string, unknown> = jest.requireActual('@/lib/integrations/platforms/github/adapter');
$   return { ...actual, fetchPullRequestByNumber: jest.fn(), fetchPullRequestReviewDecision: jest.fn() };
$ });
$ … 69 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP C13 refresh number: 7
    KWF_TEMP C13 refresh state: open
    ✓ refetches the session own PR and returns live state (59 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        10.325 s
Ran all test suites matching src/routers/kwf-c13-refresh.test.ts.
    KWF_TEMP C13 refresh number: 7
    KWF_TEMP C13 refresh state: open
Head log: backend-assert 3b0b1ecf6221 exited 0
$ cd apps/web
$ cat > src/routers/kwf-c13-refresh.test.ts <<'EOF'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests, platform_integrations } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import { createCallerForUser } from '@/routers/test-utils';
$ import * as githubAdapter from '@/lib/integrations/platforms/github/adapter';
$ jest.mock('@/lib/integrations/platforms/github/adapter', () => {
$   const actual: Record<string, unknown> = jest.requireActual('@/lib/integrations/platforms/github/adapter');
$   return { ...actual, fetchPullRequestByNumber: jest.fn(), fetchPullRequestReviewDecision: jest.fn() };
$ });
$ … 69 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP C13 refresh number: 7
    KWF_TEMP C13 refresh state: open
    ✓ refetches the session own PR and returns live state (61 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        9.758 s
Ran all test suites matching src/routers/kwf-c13-refresh.test.ts.
    KWF_TEMP C13 refresh number: 7
    KWF_TEMP C13 refresh state: open

Webhook-driven state and review-decision refresh for a verified PR is preserved, including closed/reopened/synchronize handling and pending review decisions (upsert-cli-session-pull-requests.ts:222-311).

Asserted value: KWF_TEMP C14 cache pr_state: open KWF_TEMP C14 review pending: true. Sense check (model): Both the base (3032753) and head (709af90) assert exited 0 and print 'KWF_TEMP C14 cache pr_state: open' and 'KWF_TEMP C14 review pending: true'.

Base log: backend-assert a8b6b768f153 exited 0
$ cd apps/web
$ cat > src/lib/integrations/platforms/github/webhook-handlers/kwf-c14-state.test.ts <<'EOF'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import type { PullRequestPayload } from '@/lib/integrations/platforms/github/webhook-schemas';
$ import { upsertCliSessionPullRequestsFromWebhook } from './upsert-cli-session-pull-requests';
$ const REPO = 'acme/widgets';
$ const GIT = 'https://github.com/' + REPO;
$ function makePayload(): PullRequestPayload {
$   return {
$ … 56 more line(s)
    KWF_TEMP C14 cache pr_state: open
    KWF_TEMP C14 review pending: true
    ✓ writes the cache state for a verifiably linked PR (49 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.525 s
Ran all test suites matching src/lib/integrations/platforms/github/webhook-handlers/kwf-c14-state.test.ts.
    KWF_TEMP C14 cache pr_state: open
    KWF_TEMP C14 review pending: true
Head log: backend-assert a8b6b768f153 exited 0
$ cd apps/web
$ cat > src/lib/integrations/platforms/github/webhook-handlers/kwf-c14-state.test.ts <<'EOF'
$ import { db } from '@/lib/drizzle';
$ import { cli_sessions_v2, github_branch_pull_requests } from '@kilocode/db/schema';
$ import { and, eq } from 'drizzle-orm';
$ import { insertTestUser } from '@/tests/helpers/user.helper';
$ import type { PullRequestPayload } from '@/lib/integrations/platforms/github/webhook-schemas';
$ import { upsertCliSessionPullRequestsFromWebhook } from './upsert-cli-session-pull-requests';
$ const REPO = 'acme/widgets';
$ const GIT = 'https://github.com/' + REPO;
$ function makePayload(): PullRequestPayload {
$   return {
$ … 56 more line(s)
    KWF_TEMP C14 cache pr_state: open
    KWF_TEMP C14 review pending: true
    ✓ writes the cache state for a verifiably linked PR (49 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.328 s
Ran all test suites matching src/lib/integrations/platforms/github/webhook-handlers/kwf-c14-state.test.ts.
    KWF_TEMP C14 cache pr_state: open
    KWF_TEMP C14 review pending: true

Session list, worktree view, and PR title/number search still return sessions with their verified associatedPr (cli-sessions-v2-router.ts list/get/search/worktree joins; apps/web/src/lib/active-sessions-list.ts).

Asserted value: KWF_TEMP C15 associatedPr number: 42. Sense check (jev): probability 0.91

Base log: backend-assert fe87d13e21c3 exited 0
$ cd apps/web
$ cat > src/routers/kwf-c15-associated-pr.test.ts <<'EOF'
$ import { formatAssociatedPr } from '@/routers/cli-sessions-v2-router';
$ const CACHE = {
$   pr_url: 'https://github.com/kilo/repo/pull/42',
$   pr_number: 42,
$   pr_state: 'open',
$   pr_title: 'Add feature X',
$   pr_head_sha: 'deadbeef',
$   pr_last_synced_at: '2026-01-01T00:00:00.000Z',
$   pr_review_decision: 'approved',
$   review_decision_pending: false,
$ … 23 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP C15 associatedPr number: 42
    ✓ keeps live cache state for a session with its own matching link (5 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.985 s
Ran all test suites matching src/routers/kwf-c15-associated-pr.test.ts.
    KWF_TEMP C15 associatedPr number: 42
    KWF_TEMP C15 associatedPr state: open
Head log: backend-assert fe87d13e21c3 exited 0
$ cd apps/web
$ cat > src/routers/kwf-c15-associated-pr.test.ts <<'EOF'
$ import { formatAssociatedPr } from '@/routers/cli-sessions-v2-router';
$ const CACHE = {
$   pr_url: 'https://github.com/kilo/repo/pull/42',
$   pr_number: 42,
$   pr_state: 'open',
$   pr_title: 'Add feature X',
$   pr_head_sha: 'deadbeef',
$   pr_last_synced_at: '2026-01-01T00:00:00.000Z',
$   pr_review_decision: 'approved',
$   review_decision_pending: false,
$ … 23 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP C15 associatedPr number: 42
    ✓ keeps live cache state for a session with its own matching link (4 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.563 s
Ran all test suites matching src/routers/kwf-c15-associated-pr.test.ts.
    KWF_TEMP C15 associatedPr number: 42
    KWF_TEMP C15 associatedPr state: open

Mobile Agents stored-sessions list shows the session own PR and hides a rejected unrelated-repo link

Asserted value: KWF_TEMP C1 associatedPr: none. Sense check (jev): probability 0.9

Base log: backend-assert f04fadb3469e exited 1
$ cd apps/web
$ cat > src/routers/kwf-c1-foreign-link.test.ts <<'EOF'
$ import { formatAssociatedPr } from '@/routers/cli-sessions-v2-router';
$ describe('KWF C1 read path hides an unrelated-repo link', () => {
$   it('does not show a session PR whose URL names another repo than the session git_url', () => {
$     const session = {
$       platform: 'github',
$       pr_url: 'https://github.com/someone-else/widgets/pull/2',
$       pr_number: 2,
$       git_url: 'https://github.com/acme/widgets',
$       git_branch: 'fix/typo',
$       updated_at: '2026-01-01T00:00:00.000Z',
$ … 13 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
FAIL src/routers/kwf-c1-foreign-link.test.ts
      409 |     return pendingPartialFromSession(session, opts?.partialReviewDecisionPending ?? true);
      at pr_url (src/routers/cli-sessions-v2-router.ts:406:15)
      at Object.<anonymous> (src/routers/kwf-c1-foreign-link.test.ts:14:38)
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.405 s
Ran all test suites matching src/routers/kwf-c1-foreign-link.test.ts.
Head log: backend-assert f04fadb3469e exited 0
$ cd apps/web
$ cat > src/routers/kwf-c1-foreign-link.test.ts <<'EOF'
$ import { formatAssociatedPr } from '@/routers/cli-sessions-v2-router';
$ describe('KWF C1 read path hides an unrelated-repo link', () => {
$   it('does not show a session PR whose URL names another repo than the session git_url', () => {
$     const session = {
$       platform: 'github',
$       pr_url: 'https://github.com/someone-else/widgets/pull/2',
$       pr_number: 2,
$       git_url: 'https://github.com/acme/widgets',
$       git_branch: 'fix/typo',
$       updated_at: '2026-01-01T00:00:00.000Z',
$ … 13 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP C1 associatedPr: none
  KWF C1 read path hides an unrelated-repo link
    ✓ does not show a session PR whose URL names another repo than the session git_url (3 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.485 s
Ran all test suites matching src/routers/kwf-c1-foreign-link.test.ts.
    KWF_TEMP C1 associatedPr: none

Web session list shows no PR for a linkless session whose branch has a cache row; a verified own link keeps live state and review decision

Asserted value: KWF_TEMP C2 associatedPr: none. Sense check (jev): probability 0.91

Base log: backend-assert 81c13a32850e exited 1
$ cd apps/web
$ cat > src/routers/kwf-c2-branch-fallback.test.ts <<'EOF'
$ import { formatAssociatedPr } from '@/routers/cli-sessions-v2-router';
$ describe('KWF C2 read path has no branch-name fallback', () => {
$   it('does not show a same-branch cache PR for a session with no own link', () => {
$     const session = {
$       platform: 'github',
$       pr_url: null,
$       pr_number: null,
$       git_url: 'https://github.com/acme/widgets',
$       git_branch: 'fix/typo',
$       updated_at: '2026-01-01T00:00:00.000Z',
$ … 25 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
FAIL src/routers/kwf-c2-branch-fallback.test.ts
      at Object.toBeNull (src/routers/kwf-c2-branch-fallback.test.ts:28:28)
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.499 s
Ran all test suites matching src/routers/kwf-c2-branch-fallback.test.ts.
    KWF_TEMP C2 associatedPr: 1
      27 |     console.log('KWF_TEMP C2 associatedPr: ' + (result ? String(result.number) : 'none'));
Head log: backend-assert 81c13a32850e exited 0
$ cd apps/web
$ cat > src/routers/kwf-c2-branch-fallback.test.ts <<'EOF'
$ import { formatAssociatedPr } from '@/routers/cli-sessions-v2-router';
$ describe('KWF C2 read path has no branch-name fallback', () => {
$   it('does not show a same-branch cache PR for a session with no own link', () => {
$     const session = {
$       platform: 'github',
$       pr_url: null,
$       pr_number: null,
$       git_url: 'https://github.com/acme/widgets',
$       git_branch: 'fix/typo',
$       updated_at: '2026-01-01T00:00:00.000Z',
$ … 25 more line(s)
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
      10 |   signal: () => AbortSignal.timeout(COMMAND_TIMEOUT_MS),
    KWF_TEMP C2 associatedPr: none
  KWF C2 read path has no branch-name fallback
    ✓ does not show a same-branch cache PR for a session with no own link (4 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.622 s
Ran all test suites matching src/routers/kwf-c2-branch-fallback.test.ts.
    KWF_TEMP C2 associatedPr: none

Ingest rejects a session_pr_link whose URL host/owner/repo does not match the session's git_url and stores nothing rather than a guess (ingest/metadata.ts:47-83,300-333 prUrlMatchesGitUrl gate).

Asserted value: KWF_TEMP C10 stored pr_url: absent KWF_TEMP C10 update statements: 0. Sense check (jev): probability 0.92

Base log: backend-assert b00b54b55cf4 exited 1
$ cd services/session-ingest
$ cat > src/ingest/kwf-c10-link-repo-gate.test.ts <<'EOF'
$ import { beforeEach, describe, expect, it, vi } from 'vitest';
$ vi.mock('cloudflare:workers', () => ({
$   DurableObject: class {
$     constructor() {}
$   },
$ }));
$ vi.mock('@kilocode/db/client', () => ({ getWorkerDb: vi.fn() }));
$ vi.mock('../dos/SessionAccessCacheDO', () => ({ getSessionAccessCacheDO: vi.fn() }));
$ vi.mock('../session-events', () => ({
$   mapSessionEventRow: vi.fn(() => ({
$ … 87 more line(s)
 ❯ src/ingest/kwf-c10-link-repo-gate.test.ts (1 test | 1 failed) 13ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/ingest/kwf-c10-link-repo-gate.test.ts > KWF C10 ingest drops a session_pr_link whose URL names another repo > stores nothing for a foreign-repo link and keeps the same-repo link on the ses…
AssertionError: expected [ { platform: 'github', …(2) } ] to deeply equal []
- Expected
 Test Files  1 failed (1)
      Tests  1 failed (1)
   Start at  00:38:28
   Duration  1.64s (transform 665ms, setup 0ms, import 1.49s, tests 13ms, environment 0ms)
KWF_TEMP C10 stored pr_url: https://github.com/someone-else/widgets/pull/42
KWF_TEMP C10 update statements: 1
     96|     console.log('KWF_TEMP C10 stored pr_url: ' + String(written.pr_url…
     97|     console.log('KWF_TEMP C10 update statements: ' + db.updateSets.len…
Head log: backend-assert b00b54b55cf4 exited 0
$ cd services/session-ingest
$ cat > src/ingest/kwf-c10-link-repo-gate.test.ts <<'EOF'
$ import { beforeEach, describe, expect, it, vi } from 'vitest';
$ vi.mock('cloudflare:workers', () => ({
$   DurableObject: class {
$     constructor() {}
$   },
$ }));
$ vi.mock('@kilocode/db/client', () => ({ getWorkerDb: vi.fn() }));
$ vi.mock('../dos/SessionAccessCacheDO', () => ({ getSessionAccessCacheDO: vi.fn() }));
$ vi.mock('../session-events', () => ({
$   mapSessionEventRow: vi.fn(() => ({
$ … 87 more line(s)
KWF_TEMP C10 stored pr_url: absent
KWF_TEMP C10 update statements: 0
 ✓ src/ingest/kwf-c10-link-repo-gate.test.ts (1 test) 6ms
 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  00:38:32
   Duration  1.60s (transform 663ms, setup 0ms, import 1.47s, tests 6ms, environment 0ms)
KWF_TEMP C10 stored pr_url: absent
KWF_TEMP C10 update statements: 0

@iscekic iscekic self-assigned this Sep 29, 2026
The -X ours auto-resolution kept the branch's generated journal, which
dropped main's shipped 0264_api_request_log_r2 through 0267_pink_miek
entries and reused those numbers for the branch's own migrations, so
drizzle would skip main's entries forever on any database that already
applied main's 0267. Restore main's 0264-0267 snapshots and entries,
renumber the branch's generated migrations after them as
0268_pr_link_identity and 0269_cli_sessions_git_url_pr_number_index
(content unchanged), and keep the branch's own pr-link-identity change.
@iscekic

iscekic commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Fixed the main merge in 709af90.

@iscekic
iscekic marked this pull request as ready for review October 4, 2026 14:36
@iscekic

iscekic commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Merged main in dedicated worktree; no rebase. Regenerated the branch migrations as 0269_pr_link_identity.sql after main’s shipped 0268. Preserved concurrent-index transaction boundaries. Removed source-text migration tests rather than tying them to generated file layout.

Verification: drizzle check passed; targeted database schema/journal check passed (1 test); real empty-database migration bootstrap passed. No full local suite ran.

The PR is ready for review. Waiting for Kilobot and current-head CI; I will not merge this PR.

@iscekic

iscekic commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

@kilo-code-bot Please review the current head. The PR is now ready for review, and all current-head CI checks pass.

Comment thread packages/db/src/migrations/0269_pr_link_identity.sql
Comment thread packages/db/src/migrations/0269_pr_link_identity.sql Outdated
Comment thread apps/web/src/lib/integrations/platforms/github/pr-link-identity.ts Outdated
Comment thread services/session-ingest/src/types/user-connection-protocol.ts Outdated
Comment thread apps/web/src/components/cloud-agent-next/PrHoverCardContent.tsx Outdated
@kilo-code-bot

kilo-code-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

The squash into 0269 preserves the unique-index-at-every-step fix, but the migration still drops the branch-key indexes the still-deployed webhook upsert conflicts on and does so non-concurrently, so both previously raised migration issues remain in the current changes.

Overview

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

WARNING

File Line Issue
packages/db/src/migrations/0269_pr_link_identity.sql 36 The DROP INDEX of UQ_github_branch_prs_org / UQ_github_branch_prs_user ships in the same pre-promotion migration batch that the still-deployed webhook INSERT ... ON CONFLICT targets, so those upserts raise 42P10 from the drop until promotion. The maintainer accepted this bounded, self-recovering window (PR description) and the suggested keep-both-indexes fix breaks the feature.

SUGGESTION

File Line Issue
packages/db/src/migrations/0269_pr_link_identity.sql 36 Non-concurrent DROP INDEX takes an ACCESS EXCLUSIVE lock on the populated github_branch_pull_requests cache; maintainer declined, citing the packages/db/AGENTS.md restriction on editing generated index DDL.
Files Reviewed (7 files)
  • packages/db/src/migrations/0269_pr_link_identity.sql - 2 issues
  • packages/db/src/migrations/0270_pr_cache_identity_indexes.sql - deleted (squashed into 0269)
  • packages/db/src/migrations/0271_drop_branch_pr_cache_indexes.sql - deleted (squashed into 0269)
  • packages/db/src/migrations/meta/0269_snapshot.json - generated
  • packages/db/src/migrations/meta/0270_snapshot.json - generated, deleted
  • packages/db/src/migrations/meta/0271_snapshot.json - generated, deleted
  • packages/db/src/migrations/meta/_journal.json - generated

Fix these issues in Kilo Cloud

Previous Review Summaries (2 snapshots, latest commit 0c317c3)

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

Previous review (commit 0c317c3)

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

The staged 0269-0271 migrations now keep a unique index present at each step, but 0271 still drops the branch-key indexes in the same pre-promotion batch the currently-deployed webhook upsert depends on, so its ON CONFLICT fails until the new code is live.

Overview

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

WARNING

File Line Issue
packages/db/src/migrations/0271_drop_branch_pr_cache_indexes.sql 1 Drops the old branch-key unique indexes while the still-deployed webhook code conflicts on (git_url, git_branch, owner), so every INSERT ... ON CONFLICT raises 42P10 between migration and promotion.

SUGGESTION

File Line Issue
packages/db/src/migrations/0271_drop_branch_pr_cache_indexes.sql 3 Non-concurrent DROP INDEX takes an ACCESS EXCLUSIVE lock on the populated github_branch_pull_requests cache table.
Files Reviewed (16 files)
  • apps/web/src/components/cloud-agent-next/PrHoverCardContent.tsx
  • apps/web/src/lib/integrations/platforms/github/pr-link-identity.ts
  • apps/web/src/lib/integrations/platforms/github/pr-link-identity.test.ts
  • apps/web/src/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-requests.ts
  • apps/web/src/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-requests.test.ts
  • apps/web/src/routers/cli-sessions-v2-router.ts
  • packages/db/src/migrations/0269_pr_link_identity.sql
  • packages/db/src/migrations/0270_pr_cache_identity_indexes.sql
  • packages/db/src/migrations/0271_drop_branch_pr_cache_indexes.sql
  • packages/db/src/schema.ts
  • services/session-ingest/src/types/user-connection-protocol.ts
  • services/session-ingest/src/types/user-connection-protocol.test.ts
  • packages/db/src/migrations/meta/0269_snapshot.json
  • packages/db/src/migrations/meta/0270_snapshot.json
  • packages/db/src/migrations/meta/0271_snapshot.json
  • packages/db/src/migrations/meta/_journal.json

Fix these issues in Kilo Cloud

Previous review (commit 709af90)

Status: 6 Issues Found | Recommendation: Address before merge

Executive Summary

The migration commits the drop of the github_branch_pull_requests unique indexes before rebuilding them, and it rebuilds them non-concurrently, so webhook ON CONFLICT writes can fail during deploy and on pre-existing duplicates.

Overview

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 2

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/db/src/migrations/0269_pr_link_identity.sql 7 COMMIT persists the unique-index drops before they are recreated, leaving no unique index during the concurrent build; webhook ON CONFLICT fails with 42P10 in that window.
packages/db/src/migrations/0269_pr_link_identity.sql 10 Non-concurrent unique-index rebuild write-locks a populated table, and can abort if (git_url, pr_number, owner) duplicates already exist under the old (git_url, git_branch, owner) key.
apps/web/src/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-requests.ts 133 Auto-verification gate never checks that the session's stored pr_url names evidence.gitUrl, so a legacy mismatched link can be marked verified and rendered.
services/session-ingest/src/types/user-connection-protocol.ts 77 headRef/headSha are optional but not nullable; a CLI sending null fails parse and the whole heartbeat is dropped.

SUGGESTION

File Line Issue
apps/web/src/lib/integrations/platforms/github/pr-link-identity.ts 55 PULL_URL_RE can match /pull/<n> inside a query/fragment, so parsePullRequestUrl reports a PR for a non-PR URL.
apps/web/src/components/cloud-agent-next/PrHoverCardContent.tsx 96 Pending-card copy says the link is not verified, but state === 'unknown' means it is verified and only the cache row has not synced.
Files Reviewed (39 files)
  • apps/web/src/components/cloud-agent-next/PrHoverCardContent.tsx - 1 issue
  • apps/web/src/components/cloud-agent-next/SessionPrIndicator.tsx
  • apps/web/src/components/cloud-agent-next/types.ts
  • apps/web/src/lib/active-sessions-list.test.ts
  • apps/web/src/lib/active-sessions-list.ts
  • apps/web/src/lib/integrations/platforms/github/adapter.test.ts
  • apps/web/src/lib/integrations/platforms/github/adapter.ts
  • apps/web/src/lib/integrations/platforms/github/batch-review-decisions.test.ts
  • apps/web/src/lib/integrations/platforms/github/batch-review-decisions.ts
  • apps/web/src/lib/integrations/platforms/github/pr-link-identity.test.ts
  • apps/web/src/lib/integrations/platforms/github/pr-link-identity.ts - 1 issue
  • apps/web/src/lib/integrations/platforms/github/webhook-handler-dispatch.integration.test.ts
  • apps/web/src/lib/integrations/platforms/github/webhook-handlers/supported-platforms.ts
  • apps/web/src/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-request-review.test.ts
  • apps/web/src/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-request-review.ts
  • apps/web/src/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-requests.test.ts
  • apps/web/src/lib/integrations/platforms/github/webhook-handlers/upsert-cli-session-pull-requests.ts - 1 issue
  • apps/web/src/routers/active-sessions-router.test.ts
  • apps/web/src/routers/cli-sessions-v2-router.test.ts
  • apps/web/src/routers/cli-sessions-v2-router.ts
  • apps/web/src/routers/cli-sessions-v2-worktree.test.ts
  • apps/web/src/tests/setup/__mocks__/lib/integrations/platforms/github/adapter.ts
  • dev/seed/app/session-pr-link.ts
  • packages/db/src/migrations/0269_pr_link_identity.sql - 2 issues
  • packages/db/src/migrations/meta/0269_snapshot.json
  • packages/db/src/migrations/meta/_journal.json
  • packages/db/src/schema.ts
  • services/session-ingest/src/dos/SessionIngestDO.test.ts
  • services/session-ingest/src/dos/SessionIngestDO.ts
  • services/session-ingest/src/dos/UserConnectionDO.test.ts
  • services/session-ingest/src/dos/UserConnectionDO.ts
  • services/session-ingest/src/dos/session-ingest-extractors.test.ts
  • services/session-ingest/src/dos/session-ingest-extractors.ts
  • services/session-ingest/src/ingest/metadata.test.ts
  • services/session-ingest/src/ingest/metadata.ts
  • services/session-ingest/src/types/session-sync.test.ts
  • services/session-ingest/src/types/session-sync.ts
  • services/session-ingest/src/types/user-connection-protocol.test.ts
  • services/session-ingest/src/types/user-connection-protocol.ts - 1 issue

Reviewed by deepseek-v4.1-flash · Input: 60.8K · Output: 21.6K · Cached: 658.8K

Review guidance: REVIEW.md from base branch main

- Stage the cache key migration: backfill duplicate PR rows, build the
  replacement unique indexes concurrently, then drop the branch indexes,
  so a unique index exists at every step.
- Parse PR URLs from the pathname so query or fragment text cannot fake a
  /pull/<n> segment.
- Accept null heartbeat headRef/headSha instead of dropping the heartbeat.
- Require the stored pr_url to name the webhook PR before verifying.
- Say the pending PR card has not synced, not that it is unverified.
A matching head SHA no longer verifies a session whose stored head ref or
branch names another branch. The webhook gate now applies the same branch
rule as verifySessionPullRequestLink.
Comment thread packages/db/src/migrations/0271_drop_branch_pr_cache_indexes.sql Outdated
Comment thread packages/db/src/migrations/0271_drop_branch_pr_cache_indexes.sql Outdated
Regenerate one migration from the final schema. Keep the reviewed order: backfill duplicate PR cache rows, build the replacement indexes concurrently, then drop the branch indexes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants