Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
d6757d2
fix(opencode): bind legacy attach status to its TUI pane
nwparker Oct 3, 2026
d861e9e
fix(opencode): fence legacy TUI viewers before creator attribution
nwparker Oct 3, 2026
4a9045d
fix(opencode): preserve current plugin exports in legacy TUI wrapper
nwparker Oct 3, 2026
fd3f78f
fix(opencode): keep shared-session TUI activity with its owning pane
nwparker Oct 3, 2026
e8402b4
fix(opencode): retry timed-out SSH plugin updates (#24666)
nwparker Oct 3, 2026
53f9ea7
fix(opencode): keep Go credentials private and resolve backend keys (…
nwparker Oct 3, 2026
0b15a20
Skip store notifications when refreshed work items are unchanged (#24…
nwparker Oct 3, 2026
7a3ce0b
Read project activity once while sorting the sidebar (#24549)
nwparker Oct 3, 2026
7539d19
Skip impossible link and tag matches in docs search results (#24646)
nwparker Oct 3, 2026
29e7adf
Decode complete transcript lines without copying their bytes (#24546)
nwparker Oct 3, 2026
37b1f47
Clear unused speech worker timers after errors and exit (#24924)
nwparker Oct 3, 2026
87f5a98
Reuse documentation search excerpts during result navigation (#24574)
nwparker Oct 3, 2026
abd2b49
Append subagent handoffs without recopying their message list (#24672)
nwparker Oct 3, 2026
072f25e
Cancel plugin retry timers when their fetch is replaced (#24700)
nwparker Oct 3, 2026
c0a673b
Avoid rebuilding visible file paths for single-row selection (#24743)
nwparker Oct 3, 2026
76adcce
Reuse prepared reads while searching saved agent conversations (#24535)
nwparker Oct 3, 2026
c71753b
Reuse discovered mobile chunks during static traversal (#24774)
nwparker Oct 3, 2026
14ab734
Skip unused image-size calculations in mobile web browser requests (#…
nwparker Oct 3, 2026
b109544
Skip diff analysis after a result has been discarded (#24711)
nwparker Oct 3, 2026
7bbe421
Skip late browser grab toasts after their surface closes (#24727)
nwparker Oct 3, 2026
9142169
Classify native chat waiting messages once (#24771)
nwparker Oct 3, 2026
9d68ae3
Skip discarded Kanban pruning for an empty selection (#24782)
nwparker Oct 3, 2026
cf73a97
Index discovered test files once during shard selection validation (#…
nwparker Oct 3, 2026
1198d06
Clear the watchdog benchmark heartbeat when CPU sampling fails (#24802)
nwparker Oct 3, 2026
3b13282
Reuse the parsed notification when leasing a push delivery (#24645)
nwparker Oct 3, 2026
36d0086
Update README downloads badge
github-actions[bot] Oct 3, 2026
30e0cca
Let retired-cache GC observations settle across the existing six-turn…
nwparker Oct 3, 2026
f93808d
Collect test-selection evidence when full unit tests fail (#24955)
nwparker Oct 3, 2026
ebe7702
Check each project repository once while filtering mobile cards (#24540)
nwparker Oct 3, 2026
d2c9636
Skip renderer callbacks after their request has ended (#24631)
nwparker Oct 3, 2026
787b9ad
fix(opencode): bundle the relay JSONC parser as ESM
nwparker Oct 3, 2026
9e11375
Decode single-piece saved-session tails without copying them (#24632)
nwparker Oct 3, 2026
832caea
Avoid irrelevant scroll-action searches in Mac snapshots (#24731)
nwparker Oct 3, 2026
1c8d281
Stop copying every retained browser page before attachment (#24553)
nwparker Oct 3, 2026
0afc7b1
Reuse event byte sizes when relay watchers notify several clients (#2…
nwparker Oct 3, 2026
cca14bd
Stop building unused import candidates in the test planner (#24611)
nwparker Oct 3, 2026
c792386
docs(terminal): explain local macOS/Linux shell startup files
brynnclaw Oct 3, 2026
97fd7ed
fix(editor): recognize Ruby task and configuration files
ggbdpq Oct 3, 2026
b332742
Speed up large Markdown Find and render oversized tables (#24948)
nwparker Oct 3, 2026
fa2d50f
fix(source-control): generate clean OpenCode messages locally and ove…
nwparker Oct 3, 2026
b2a2ee5
Fix OpenCode legacy TUI session identity and relay parser bundling
nwparker Oct 3, 2026
81e69f3
fix(persistence): preserve projectGroupOrder on restart for flat fold…
lurunzi Oct 3, 2026
6565a3f
fix(runtime): reclaim temp files orphaned by interrupted mobile store…
LDH1103 Oct 3, 2026
8c4d63e
fix(terminal): show actionable copy for missing folder workspace paths
Waynting Oct 3, 2026
6486624
docs: reference local orca.yaml and .worktreeinclude
nwparker Oct 3, 2026
4f46f5b
fix(orchestration): allow model selection for OMP workers
nwparker Oct 3, 2026
c39f5f2
fix(cli): preserve primitive success results in JSON output
nwparker Oct 3, 2026
f255574
Show loaded file changes before deleting a workspace
nwparker Oct 3, 2026
0f1bbce
fix(keybindings): record and match Option+digit shortcuts on macOS
marcuslannister Oct 3, 2026
9f5c520
fix(editor): don't throw closing a stale active file
OrcaWin Oct 3, 2026
5f5c266
Fix Project Settings targeting for multiple local checkouts
fsmeier Oct 3, 2026
130b2ef
feat(documents): open CSV and TSV files from the OS
nwparker Oct 3, 2026
c1d403a
feat(cli): link GitHub and GitLab items on worktree create and set
SongMarco Oct 3, 2026
7b8498b
feat(sidebar): include folder workspaces in keyboard navigation
nwparker Oct 3, 2026
126f709
fix(build): turn off MSBuild file tracking for Windows native rebuilds
b1nhm1nh Oct 3, 2026
f113d35
Fix Ctrl+M in Linux and Windows terminals
yuzhichang Oct 3, 2026
98a6d20
fix(startup): read a nushell login PATH from $env.PATH
Kh05ifr4nD Oct 3, 2026
8919f3d
fix(cursor): run local hooks through sh for non-POSIX login shells
Kh05ifr4nD Oct 3, 2026
a5f28f2
Fix word wrap for both panes in side-by-side diffs
innocarpe Oct 3, 2026
df9acd8
fix(monaco): highlight Svelte block closers inside markup
innocarpe Oct 3, 2026
752dbc9
fix(repos): keep active clone dialog open on outside clicks
nwparker Oct 3, 2026
8ccc336
fix(editor): keep chat visible after closing Markdown tabs
nwparker Oct 3, 2026
44c584e
Honor typed starting numbers in chat ordered lists
nwparker Oct 3, 2026
4a5f00f
Normalize base source once while checking changed-code diagnostics (#…
nwparker Oct 3, 2026
a2896f5
Support real OpenCode sessions in native Chat (#24647)
nwparker Oct 3, 2026
ec47f0c
Drain removal fixture jobs before resetting and deleting their record…
nwparker Oct 3, 2026
668d6d4
Reuse measured Electron preparation for current Terminal Perf refs (#…
nwparker Oct 3, 2026
4049e63
feat(jcode): add Jcode as a supported TUI agent with managed hooks
czzczz Sep 23, 2026
2ca85d2
feat(jcode): evidence-backed status pipeline (turn_start, live pre_to…
nwparker Sep 23, 2026
6164d3f
fix(jcode): register vault fixture, dynamic model discovery, script r…
nwparker Sep 23, 2026
5b43e20
fix(jcode): keep finished-turn detail so completion notifications fire
nwparker Sep 23, 2026
5ab36cc
fix(jcode): show the prompt in agent rows instead of jcode's repainti…
nwparker Sep 23, 2026
33cab24
fix(jcode): pre-warm the per-pane daemon so a cold runtime dir cannot…
nwparker Sep 23, 2026
7fb201f
fix(jcode): stop the OSC color skip from crashing every pane connect
nwparker Sep 23, 2026
0a45ad5
refactor(jcode): fold three reverse-scan copies into one, reuse the s…
nwparker Sep 23, 2026
4f2007a
fix(jcode): narrow dynamic reads with predicates, pin the Windows hoo…
nwparker Sep 23, 2026
2c04f79
test(jcode): measure the gate against the synchronous path, not the c…
nwparker Sep 24, 2026
888e3c4
test(jcode): drop the wall-clock gate assertion
nwparker Sep 24, 2026
a7b9358
fix(jcode): address CodeRabbit review on #22539
nwparker Sep 24, 2026
fe76440
fix(jcode): read the journal once per turn, key prompts by byte offset
nwparker Sep 24, 2026
4c2cb3c
fix(jcode): quote the managed hook path, drop tools from patch prompts
nwparker Sep 24, 2026
07d8aff
test(jcode): pin the tool-profile flag in the generation plan too
nwparker Sep 24, 2026
154b2fd
fix(text-generation): refuse an oversized argv prompt on Linux too
nwparker Sep 24, 2026
84d246b
fix(jcode): register in main's remote-installer guard, drop our dupli…
nwparker Oct 3, 2026
af4c3e2
fix(jcode): decompose the four files jcode pushed over max-lines
nwparker Oct 3, 2026
8dc16a7
feat(native-chat): open structured chats on the paired Orca server th…
brennanb2025 Oct 3, 2026
2fc517c
feat(cli): add --unread/--read to orca worktree set
razshlomo Oct 3, 2026
bde1c09
feat(cli): configure external worktree visibility per repo
nwparker Oct 3, 2026
2fb16dc
Keep legacy OpenCode pane reports isolated through actual plugin loaders
nwparker Oct 3, 2026
a40e501
perf(git): reuse queries and stop canceled catalog scans (#24923)
nwparker Oct 3, 2026
3a0aac5
Preserve current main while fixing legacy OpenCode shared pane attrib…
nwparker Oct 3, 2026
db7e9fc
Clarify OpenCode generation comment after real legacy catalog probe
nwparker Oct 3, 2026
1001118
test(e2e): require exact Linux package tokens (#24995)
nwparker Oct 3, 2026
0e36159
Allow cold node-pty setup on Windows ARM CI (#24997)
nwparker Oct 3, 2026
9aeeeb5
Fence legacy OpenCode relay posts with execution-host launch identity
nwparker Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ jobs:
# after the last shard. Deliberately absent from verify's needs for the same reason.
unit_selection_evidence:
needs: [test]
if: ${{ !cancelled() && needs.test.result == 'success' }}
if: ${{ !cancelled() && (needs.test.result == 'success' || needs.test.result == 'failure') }}
uses: ./.github/workflows/unit-selection-evidence.yml

# Why a separate job: the test needs a real Chrome, and the sharded `test` matrix
Expand Down
39 changes: 37 additions & 2 deletions .github/workflows/terminal-perf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,25 +68,60 @@ jobs:
- name: Install native build tools and xvfb
run: sudo apt-get update && sudo apt-get install -y build-essential python3 xvfb zsh

- name: Select dependency preparation
id: install-mode
shell: bash
env:
RUNNER_KIND: ${{ runner.environment }}
JOB_CONTAINER: ${{ job.container.id }}
run: |
node <<'NODE'
const fs = require('node:fs')
const manifest = JSON.parse(fs.readFileSync('package.json', 'utf8'))
const actionPath = '.github/actions/install-node-dependencies/action.yml'
const action = fs.existsSync(actionPath) ? fs.readFileSync(actionPath, 'utf8') : ''
const inputs = action.split(/^inputs:[ \t]*\r?$/m)[1]?.split(/^\S/m)[0] ?? ''
const shared = process.env.RUNNER_KIND === 'github-hosted' && !process.env.JOB_CONTAINER &&
process.env.RUNNER_OS === 'Linux' && process.env.RUNNER_ARCH === 'X64' &&
manifest.engines?.node === '24' && typeof manifest.packageManager === 'string' &&
manifest.packageManager.split('+')[0] === 'pnpm@12.8.1' &&
manifest.scripts?.postinstall === 'node config/scripts/rebuild-native-deps.mjs' &&
/^ native-runtime:/m.test(inputs) && /^ cache-pnpm-store-lookup-only:/m.test(inputs) &&
fs.existsSync('.github/actions/prepare-native-runtime/action.yml') &&
fs.existsSync('config/scripts/ensure-native-runtime.mjs')
fs.appendFileSync(process.env.GITHUB_OUTPUT, `shared=${shared}\n`)
NODE

- name: Prepare current dependencies
if: steps.install-mode.outputs.shared == 'true'
uses: ./.github/actions/install-node-dependencies
with:
native-runtime: electron
cache-electron-package: 'true'
cache-pnpm-store-lookup-only: 'true'

- name: Setup pnpm
if: steps.install-mode.outputs.shared != 'true'
uses: pnpm/setup@v2
with:
install: false

- name: Setup Node.js
if: steps.install-mode.outputs.shared != 'true'
uses: actions/setup-node@v6
with:
node-version-file: package.json
cache: pnpm

# Why: this scheduled/manual workflow uses the same native install path as
# PR and E2E CI, which needs pnpm to bypass its bundled gyp_main.py.
# Historical refs can lack the shared action; retain their original install path.
- name: Use external node-gyp to avoid pnpm's bundled copy
if: steps.install-mode.outputs.shared != 'true'
run: |
npm install -g node-gyp@11.5.0
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"

- name: Install dependencies
if: steps.install-mode.outputs.shared != 'true'
run: pnpm install --frozen-lockfile

- name: Build Electron app for terminal perf
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ docs/**
!docs/reference/git-compatibility.md
!docs/reference/headless-linux-server.md
!docs/reference/ime-regression-checklist.md
!docs/reference/jcode-hook-events.md
!docs/reference/linux-glibc-compatibility.md
!docs/reference/macos-press-and-hold.md
!docs/reference/orcad-operations.md
Expand Down
244 changes: 244 additions & 0 deletions cloud/apps/push/src/durable-push-payload-parse-budget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { createHash } from 'node:crypto'
import { afterEach, expect, it, vi } from 'vitest'
import { DurablePushStore } from './durable-push-store.js'
import { buildPushDelivery } from './push-delivery-message.js'
import type { PushDatabase } from './push-database.js'
import {
cleanupDurablePushFixtures,
fixture,
notification
} from './durable-push-store.test-fixture.js'

type QueryCall = { sql: string; params?: unknown[] }

afterEach(async () => {
vi.restoreAllMocks()
await cleanupDurablePushFixtures()
})

function traceDatabase(
database: PushDatabase,
calls: QueryCall[],
errors: unknown[]
): PushDatabase {
return {
dialect: database.dialect,
query: async (sql, params) => {
calls.push({ sql, params })
try {
return await database.query(sql, params)
} catch (error) {
errors.push(error)
throw error
}
},
transaction: (run) => database.transaction((tx) => run(traceDatabase(tx, calls, errors))),
lockQuotaScope: (key) => database.lockQuotaScope(key),
tryLockScope: (key) => database.tryLockScope(key),
tryLockSharedScope: (key) => database.tryLockSharedScope(key),
close: () => database.close()
}
}

function payloadHash(value: unknown): string {
return createHash('sha256').update(JSON.stringify(value)).digest('hex')
}

it('parses each leased row once with exact complete payload, serialized key order and SQL sequence', async () => {
const { db, store, clock } = await fixture()
const input = {
...notification(1),
body: 'Unicode: 🐋\ud800',
extra: { first: [null, false, 3], next: { z: 'last', a: 'first' } }
}
await store.accept('host', 'phone', input)
const [row] = await db.query('SELECT * FROM push_delivery_batches')
if (!row) {
throw new Error('Missing delivery row')
}
const payload = String(row.payload_json)
const calls: QueryCall[] = []
const errors: unknown[] = []
const owner = new DurablePushStore(traceDatabase(db, calls, errors), clock)
const parse = vi.spyOn(JSON, 'parse')
const delivery = await owner.claim()
expect(delivery).toEqual({
id: row.batch_id,
registrationId: 'phone',
hostFingerprint: 'host',
notification: input,
expiresAt: 1_300_000,
lease: expect.any(String),
attempts: 1
})
expect(JSON.stringify(delivery?.notification)).toBe(payload)
expect(payloadHash(delivery?.notification)).toBe(payloadHash(input))
if (!delivery) {
throw new Error('Missing delivery')
}
const published = buildPushDelivery(delivery)
const expected = buildPushDelivery({ ...delivery, notification: input })
expect(JSON.stringify(published)).toBe(JSON.stringify(expected))
expect(payloadHash(published)).toBe(payloadHash(expected))
expect(calls.map(({ sql }) => sql.replace(/\s+/g, ' ').trim())).toEqual([
`SELECT * FROM push_delivery_batches WHERE state = 'pending' AND lease_until <= ? AND expires_at > ? AND due_at <= ? AND due_at > ? AND NOT EXISTS (SELECT 1 FROM push_delivery_batches busy WHERE busy.registration_id = push_delivery_batches.registration_id AND busy.state = 'pending' AND busy.lease_until > 0 AND busy.lease_until > ?) ORDER BY due_at, created_at, batch_id LIMIT 1${db.dialect === 'postgres' ? ' FOR UPDATE SKIP LOCKED' : ''}`,
"SELECT (SELECT batch_id FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending' AND expires_at > ? AND due_at > ? ORDER BY due_at, created_at, batch_id LIMIT 1) AS head, EXISTS (SELECT 1 FROM push_delivery_batches WHERE registration_id = ? AND state = 'pending' AND lease_until > 0 AND lease_until > ?) AS busy",
'SELECT notification_seq FROM push_dismissed_events WHERE host_fingerprint = ? AND notification_epoch = ? AND notification_id = ?',
'UPDATE push_delivery_batches SET lease_token = ?, lease_until = ?, attempts = attempts + 1 WHERE batch_id = ?'
])
expect(calls[1]?.params).toEqual(['phone', clock(), clock() - 300_000, 'phone', clock()])
expect(calls[2]?.params).toEqual(['host', 'epoch', 'notification-1'])
expect(calls[3]?.params).toEqual([delivery?.lease, clock() + 30_000, row.batch_id])
expect(errors).toEqual([])
expect(parse.mock.calls.filter(([value]) => value === payload)).toHaveLength(1)
})

it('keeps concurrent device claims separate and returns fresh payload objects', async () => {
const { store } = await fixture()
const input = notification(1)
await store.accept('host', 'phone-a', input)
await store.accept('host', 'phone-b', input)
const payload = JSON.stringify(input)
const parse = vi.spyOn(JSON, 'parse')
const claims = await Promise.all(Array.from({ length: 4 }, () => store.claim()))
const delivered = claims.filter((claim) => claim !== null)
expect(delivered).toHaveLength(2)
expect(delivered.map((claim) => claim.registrationId).sort()).toEqual(['phone-a', 'phone-b'])
expect(delivered.every((claim) => JSON.stringify(claim.notification) === payload)).toBe(true)
expect(delivered[0]?.notification).not.toBe(delivered[1]?.notification)
expect(parse.mock.calls.filter(([value]) => value === payload)).toHaveLength(2)
})

it('reads changed retry bytes and a later writer update without carrying a parsed result across calls', async () => {
const { db, store, advance } = await fixture()
await store.accept('host', 'phone', notification(1))
const first = await store.claim()
if (!first) {
throw new Error('Missing first delivery')
}
first.notification.body = 'provider changed this retry'
await store.finish(first, 1000)
advance(1000)
const retriedPayload = JSON.stringify(first.notification)
const parse = vi.spyOn(JSON, 'parse')
const retry = await store.claim()
expect(retry).toEqual({ ...first, lease: expect.any(String), attempts: 2 })
expect(retry?.lease).not.toBe(first.lease)
expect(retry?.notification).not.toBe(first.notification)
expect(JSON.stringify(retry?.notification)).toBe(retriedPayload)
expect(payloadHash(retry?.notification)).toBe(payloadHash(first.notification))
const retryParses = parse.mock.calls.filter(([value]) => value === retriedPayload).length
if (!retry) {
throw new Error('Missing retry delivery')
}
await store.finish(retry, 1000)
const changed = { ...notification(1), body: 'fresh database row', title: 'Changed' }
const changedPayload = JSON.stringify(changed)
await db.query('UPDATE push_delivery_batches SET payload_json = ? WHERE batch_id = ?', [
changedPayload,
first.id
])
advance(1000)
const fresh = await store.claim()
expect(fresh).toEqual({ ...retry, notification: changed, lease: expect.any(String), attempts: 3 })
expect(JSON.stringify(fresh?.notification)).toBe(changedPayload)
expect(payloadHash(fresh?.notification)).toBe(payloadHash(changed))
expect(retry.notification.body).toBe('provider changed this retry')
expect(retryParses).toBe(1)
expect(parse.mock.calls.filter(([value]) => value === changedPayload)).toHaveLength(1)
})

it('keeps dismissed alerts on the original single-parse delete path without leasing', async () => {
const { db, store, clock } = await fixture()
const input = notification(1)
await store.accept('host', 'phone', input)
await db.query(
'INSERT INTO push_dismissed_events(host_fingerprint, notification_epoch, notification_id, notification_seq, created_at) VALUES (?, ?, ?, ?, ?)',
['host', 'epoch', input.notificationId, 1, clock()]
)
const calls: QueryCall[] = []
const parse = vi.spyOn(JSON, 'parse')
expect(await new DurablePushStore(traceDatabase(db, calls, []), clock).claim()).toBeNull()
expect(await store.pendingCount('phone')).toBe(0)
expect(calls.at(-1)?.sql).toBe('DELETE FROM push_delivery_batches WHERE batch_id = ?')
expect(calls.some(({ sql }) => sql.startsWith('UPDATE'))).toBe(false)
expect(parse.mock.calls.filter(([value]) => value === JSON.stringify(input))).toHaveLength(1)
})

it('preserves the existing trust boundary for an object missing notification fields', async () => {
const { db, store } = await fixture()
await store.accept('host', 'phone', notification(1))
await db.query('UPDATE push_delivery_batches SET payload_json = ?', ['{}'])
const parse = vi.spyOn(JSON, 'parse')
const delivery = await store.claim()
expect(delivery?.notification).toEqual({})
expect(JSON.stringify(delivery?.notification)).toBe('{}')
expect(parse.mock.calls.filter(([value]) => value === '{}')).toHaveLength(1)
})

it.each(['not JSON', 'undefined', 'null', '[]'])(
'preserves invalid payload rejection and rolls back the lease for %s',
async (payload) => {
const { db, store } = await fixture()
await store.accept('host', 'phone', notification(1))
await db.query('UPDATE push_delivery_batches SET payload_json = ?', [payload])
const [before] = await db.query('SELECT * FROM push_delivery_batches')
const parse = vi.spyOn(JSON, 'parse')
let caught: unknown
try {
await store.claim()
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Error)
const index = parse.mock.calls.findIndex(([value]) => value === payload)
expect(index).toBeGreaterThanOrEqual(0)
if (payload === 'not JSON' || payload === 'undefined') {
expect(caught).toBe(parse.mock.results[index]?.value)
expect(caught).toBeInstanceOf(SyntaxError)
} else {
expect(caught).toMatchObject({ message: 'invalid_push_delivery_payload' })
}
expect(await db.query('SELECT * FROM push_delivery_batches')).toEqual([before])
expect(parse.mock.calls.filter(([value]) => value === payload)).toHaveLength(1)
}
)

it('preserves the exact database UPDATE error and retries with a fresh payload after rollback', async () => {
const { db, store, clock } = await fixture()
await store.accept('host', 'phone', notification(1))
const [before] = await db.query('SELECT * FROM push_delivery_batches')
if (!before) {
throw new Error('Missing delivery row')
}
const originalQuery = db.query.bind(db)
const errors: unknown[] = []
// SQLite raises a native error in the real transaction; PostgreSQL uses its real constraint.
await originalQuery(
db.dialect === 'sqlite'
? "CREATE TRIGGER deny_lease BEFORE UPDATE ON push_delivery_batches BEGIN SELECT RAISE(FAIL, 'deny_lease'); END"
: 'ALTER TABLE push_delivery_batches ADD CONSTRAINT deny_lease CHECK (lease_until = 0)'
)
const owner = new DurablePushStore(traceDatabase(db, [], errors), clock)
const parse = vi.spyOn(JSON, 'parse')
let caught: unknown
try {
await owner.claim()
} catch (error) {
caught = error
}
expect(errors).toHaveLength(1)
expect(caught).toBe(errors[0])
expect(await originalQuery('SELECT * FROM push_delivery_batches')).toEqual([before])
expect(parse.mock.calls.filter(([value]) => value === String(before.payload_json))).toHaveLength(
1
)
await originalQuery(
db.dialect === 'sqlite'
? 'DROP TRIGGER deny_lease'
: 'ALTER TABLE push_delivery_batches DROP CONSTRAINT deny_lease'
)
const fresh = await owner.claim()
expect(fresh?.notification).toEqual(notification(1))
expect(fresh?.attempts).toBe(1)
})
6 changes: 3 additions & 3 deletions cloud/apps/push/src/durable-push-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,15 @@ export class DurablePushStore {
'UPDATE push_delivery_batches SET lease_token = ?, lease_until = ?, attempts = attempts + 1 WHERE batch_id = ?',
[lease, now + DELIVERY_LEASE_MS, row.batch_id]
)
return this.delivery(row, lease)
return this.delivery(row, lease, notification)
}

private delivery(row: SqlRow, lease: string): QueuedPushDelivery {
private delivery(row: SqlRow, lease: string, notification: PushNotification): QueuedPushDelivery {
return {
id: String(row.batch_id),
registrationId: String(row.registration_id),
hostFingerprint: String(row.host_fingerprint),
notification: parsePushDeliveryPayload(String(row.payload_json)),
notification,
expiresAt: Number(row.expires_at),
lease,
attempts: Number(row.attempts) + 1
Expand Down
6 changes: 6 additions & 0 deletions config/build-plugins/jsonc-parser-esm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { resolve } from 'node:path'

// UMD relative requires cannot survive a self-contained bundle.
export const JSONC_PARSER_ESM_ALIAS = {
'jsonc-parser': resolve(import.meta.dirname, '../../node_modules/jsonc-parser/lib/esm/main.js')
}
1 change: 1 addition & 0 deletions config/build-plugins/plain-node-entry-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type OutputChunk = Rollup.OutputChunk
// The CLI loads these paths after electron-vite replaces out/main.
export const CLI_MAIN_ENTRY_NAMES = [
'agent-hooks/managed-agent-hook-controls',
'gitlab/project-ref-parser',
'orca-profiles/profile-index-store',
'claude-accounts/keychain',
...[
Expand Down
Loading
Loading