Repository navigation
Conversation
AI disclosure: Used Opus 5.5
|
/review AI review started. The review job timed out. |
Looks like this will need a local review with |
Opus 5.5 ReviewCode ReviewBase: Unreviewed filesThe worker for this slice failed (a tool timeout), so these files were not reviewed:
1. 🟡 After a recovery-sync timeout, the 30 s bound stops applying once the height has been reached
const reached = required !== undefined && isHeightReached(ctx, contractID, required)
...
if (reached) resolve()
"The height was reached but the sync is still running" is the typical state of a slow sync: it has applied events up to
If the timeout rejected, C's onTimeout: ({ reject }) => {
console.warn(
`[chelonia/kv] contract sync for ${contractID} did not finish within ${recoveryTimeoutMs} ms; ` +
'giving up on height recovery (a retry would queue behind it)'
)
reject(error)
}Single-key 2. 🟡 Wrapped height errors are still the app's job, and the migration guide contradicts itself
Following the first two instructions mechanically silently loses detection above any app wrapper. To make the guard fully redundant, ship the walk: export a bounded, cycle-safe finder next to the predicates, use it in // src/error-names.ts
export function findErrorByName<N extends string> (
e: unknown, ...names: N[]
): (Error & { name: N }) | undefined {
const seen = new Set<unknown>()
for (let cur = e, depth = 0; cur && typeof cur === 'object' && depth < 8; depth++) {
if (seen.has(cur)) return undefined
seen.add(cur)
if (hasErrorName(cur, ...names)) return cur
cur = (cur as { cause?: unknown }).cause
}
return undefined
}
// src/errors.ts
export const findKvHeightAhead = (e: unknown) => findErrorByName(e, 'ChelErrorKvHeightAhead')
export const findKvConflict = (e: unknown) =>
findErrorByName(e, 'ChelErrorKvConflict', 'ChelErrorKvMaxAttempts')
// src/kv-height.ts (withHeightRecovery): rethrow the caller's own `e`
const ahead = findKvHeightAhead(e)
if (!ahead || mode === 'reject' || recoveries >= maxRecoveries) throw e
...
await recoverContractHeight(ctx, contractID, e, {
callerSignal: signal, sessionSignal, requiredHeight: kvHeightAheadCause(ahead)?.requiredHeight
})Then:
3. 🟡
|
|
Note the unreviewed file it mentions:
|
A caller's abort signal now rejects update, clear and queuedSet at once while their work waits behind a busy contract lane or a recovery sync, so a write awaited from another contract's lane has a bound. queuedSet and withHeightRecovery reject a synchronous call from a same-contract onUpdate instead of hanging. Also: after a stale-height rejection, kv/set waits for a running catch-up sync instead of re-uploading at every replayed height, rejects an invalid maxAttempts, and reports only the final response's etag. Deferred reloads that can't run keep the slot's "newer value exists" marker and settle the slot. A reset whose teardown throws no longer leaves height recovery broken for later sessions. Permanently removing a contract that never finished syncing is announced. The recovery types are exported, and the docs describe in one place how height recovery runs and where it must not be awaited. Assisted-by: Crush:claude-opus-5-5
When a contract-sync race test regresses, it now fails in milliseconds and reports the actual cause, instead of timing out after seconds with a message that blames the wrong step. Every wait in those race tests now has a bound, and timeout messages from the shared test helpers read correctly however the awaited condition is worded. The built test helpers are also kept out of the published package: they were never importable, but they pulled test-runner modules into it. 💘 Generated with Crush Assisted-by: Crush:claude-opus-5-5
The compiled package output now includes the KV write abort and edge-case fixes, and no longer tracks the built test helpers, which are now ignored. Assisted-by: Crush:claude-opus-5-5
chelonia/reset now cancels a queuedSet that is still queued, so its write can no longer go out after the reset. A KV write whose queued work never runs rejects instead of resolving undefined: aborted by the reset, or vetoed by a filter. Raw withHeightRecovery callers get the same guarantees by queueing through the queue function each attempt receives. A clear that runs out of attempts on a value this client can't verify yet now marks the slot as holding a newer value and reloads it later, instead of leaving it looking current or empty. Contending blind writes back off between retries again. A slot load that a filter vetoes settles the slot, and whenSettled no longer waits forever in two edge cases. null height-recovery options mean the default, and very long height waits no longer end at once. A reset whose teardown throws still starts a new KV session and restarts clock sync, and height waiters are woken even when a later step of event processing throws. kv/set without data or onconflict now fails before sending anything, with a clearer message. The docs now say correctly which code holds a contract's lanes: raw pubsub handlers and event listeners aren't awaited, so they may await KV writes. Tests that could hang now fail with a timeout instead, and assertions match what their test names say. Assisted-by: Crush:claude-opus-5-5
Opus 5.5 generated PR descriptionPR #107: what it does and whyWhyGroup Income kept hitting the same limits in The guard worked around three problems:
What it changesErrors
Built-in height recovery
Empty vs. not loaded yet
Safety around lifecycle
Supporting work
|
A slot no longer settles as empty while the server is known to hold a newer value this client can't verify yet, and whenSettled no longer resolves before anything loads for an entry persisted mid-reload. A contract resync keeps slot values, so a value removed meanwhile is reported. An ephemeral retain or withRetained whose first sync fails gives its reference back, so pending whenSettled calls reject. Writes waste fewer requests: a stale-height rejection during a long catch-up sync no longer re-uploads at every replayed height, and a height recovery loads each recovered value once. The data-loss guard's reload is reported like any load, so the slot leaves 'error'. queuedSet refuses to write plaintext when its encryption key is missing, as slot writes do. A reset cancels an update whose reload was running, and rejects pending whenSettled calls even when it fails. Filter vetoes are reported consistently. The migration docs now warn that an app-level height guard left in place silently stops working, and the docs and spec match the code. Tests that could hang are bounded, and weak assertions are tightened. Assisted-by: Crush:claude-opus-5-5
| // As for slot writes, a missing named encryption key rejects with | ||
| // `ChelErrorKvUpdateInvalid` instead of writing plaintext; pass | ||
| // `encryptionKeyName: null` to store plaintext. | ||
| // Raw `chelonia/kv/set` never syncs the contract, though: wrap such a call | ||
| // in `chelonia/kv/withHeightRecovery`, as this selector does (see | ||
| // docs/kv.md). | ||
| // | ||
| // When the server value is ahead of the local contract | ||
| // (`ChelErrorKvHeightAhead`), the write waits (holding no lane) for a sync | ||
| // of the contract, which runs on the contract's internal lane, and is | ||
| // retried (`onHeightAhead: 'sync'`, the default), up to | ||
| // `maxHeightRecoveries` times, without `kv/set`'s passive wait for the | ||
| // height, except on the last attempt, where that wait is the write's only | ||
| // chance. Pass `onHeightAhead: 'reject'` to get the error at once, without | ||
| // waiting or syncing: nothing retries the write then, so use it only | ||
| // when the caller re-issues the write itself. | ||
| // Pass `maxHeightRecoveries: 0` to wait passively without syncing. See | ||
| // KV-REVAMPED.md §4.2 step 5a. A write that recovers is enqueued again, | ||
| // so a later write to the same key can commit before it (as with an | ||
| // app-level retry). `signal` and `chelonia/reset` end the write at once | ||
| // while it is queued (it then sends nothing), and a write whose queued | ||
| // work never runs rejects (see `KvHeightRecoveryAttempt`'s `queue`). | ||
| // Awaited from code that the contract's lanes await (one of its | ||
| // `sideEffect`s, a `queueInvocation` body for it, an `onconflict`, an | ||
| // `onUpdate` of one of its slots), it never settles; a synchronous | ||
| // call from such an `onUpdate` rejects with `ChelErrorKvReentrant` (see | ||
| // docs/kv.md, "How height recovery runs"). | ||
| // `allowUnverifiedConflict` is passed on to `chelonia/kv/set`. | ||
| // | ||
| // Running out of `maxAttempts` resolving `412` conflicts, or a `412` | ||
| // without `onconflict`, rejects with `ChelErrorKvConflict`, like | ||
| // `chelonia/kv/update` and `chelonia/kv/clear`; match it with | ||
| // `isKvConflict(e)`. Its `.cause` is `kv/set`'s: `{ currentStatus, | ||
| // currentData, requiredHeight, etag }` from the last server response, with | ||
| // `currentData` only when that value is known (`'present'` and decodable, | ||
| // or `'absent'`), `requiredHeight` (the value's height stamp) only when it | ||
| // is `'ahead'`, and `currentStatus` only when the last response could be | ||
| // read and decoded. The exception is a conflict whose last value is ahead of | ||
| // the local contract (with an `onconflict`, and without | ||
| // `allowUnverifiedConflict`): `kv/set` rejects with | ||
| // `ChelErrorKvHeightAhead`, which is recovered from as above (and is the | ||
| // rejection once the recoveries run out). A blind write | ||
| // (`allowUnverifiedConflict`) doesn't depend on the value, so that is a | ||
| // conflict too. | ||
| 'chelonia/kv/queuedSet': async ({ |
There was a problem hiding this comment.
This comment is way too long. Check other places too for similar nonsense.
The height tests ran as one file that no longer fit in the 60 s limit Node 22's test runner applies to each file, so CI failed. They are now split by topic into eight files, each well under the limit, and AGENTS.md says what the limit bounds on each Node line. update no longer falls back to its last-known value when a reload fails in the cases where that is unsafe: under the caller's own ifMatch, or once it has seen a newer server value. Either could silently drop the write or overwrite another device's value. Also: single-key kv/sync no longer overwrites a newer outcome when it gives up. A slot load that a filter vetoes, or keeps from running, settles the slot, and single-key kv/sync then rejects instead of reporting success without loading. A failed reset no longer leaves a slot 'loading' for good. A vetoed queueInvocation, and a queuedSet whose signing key is missing, reject with library errors instead of a TypeError. kv/set without onconflict makes no extra GET after a 412. Removing a contract that isn't in state is quiet, and a contract whose re-sync failed gets its destructor called on final removal, like one whose re-sync succeeded. Resync status events carry the contract type, and entries of slots removed during a resync are dropped. The docs and spec match the new behaviour, the API reference's long entries link to the guide instead of copying it, and several timing-sensitive tests no longer flake on a busy machine. Assisted-by: Crush:claude-opus-5-5
|
/review {{kimi}} AI review started. |
|
EDIT by @taoeffect: The Kimi plan got used up with that one review. I had 11% remaining when it started, so it's theoretically possible it might've finished with 0% usage at the start, but this would mean either way that we would get at most 1 kimi review per 5 hours with this plan. Advanced AI Review
Click to expand reviewAgentic review unavailable (crush produced no review). Review generated using |
|
/review {{kimi}} - let me try one more time with 0% usage to see what happens AI review started. |
Advanced AI Review
Click to expand reviewAgentic review unavailable (crush produced no review). Review generated using |
This PR addresses some of the feedback in okTurtles/group-income#3137.
In particular, okTurtles/group-income#3137 introduces a new file, frontend/controller/actions/kv-guard.js, to address some limitations in the KV API here in
@chelonia/libregarding slots (for example, telling apart those that are empty from those that are uninitialized).Those workarounds don't belong in Group Income, as we can expect that other applications will run into exactly the same limitations. This PR should make those guards redundant by directly addressing those concerns in the KV API.
File in question
AI disclosure: Used Opus 5.5