InMemoryJobInventory.list interprets the wire cursor as a non-negative integer offset into the principal's sorted job list at lib/src/main/kotlin/dev/arcp/runtime/JobInventory.kt:47 (cursor?.toIntOrNull()?.coerceAtLeast(0) ?: 0), and the nextCursor it returns at lib/src/main/kotlin/dev/arcp/runtime/JobInventory.kt:56 is just start + page.size. Because the visible list is sorted by createdAt and then jobId at lib/src/main/kotlin/dev/arcp/runtime/JobInventory.kt:54, any new job recorded between two list calls shifts the indices that came after it, so the second page either repeats or skips entries depending on where the new record lands. A malformed cursor — anything not parsable as an int — silently restarts at zero rather than failing with INVALID_ARGUMENT, which both hides bugs and lets a client unknowingly re-walk the same prefix forever.
Fix prompt: Encode the cursor as a stable opaque token tied to the sort key, for example a base64-encoded (createdAt, jobId) tuple of the last entry returned, and resume by skipping past that key. Validate the cursor on input and throw ARCPException.InvalidArgument("cursor") (which the dispatch path will then translate to a correlated Nack) when the value cannot be decoded. Add tests in lib/src/test/kotlin/dev/arcp/runtime/ListJobsHandlerTest.kt that record a job between two list calls and assert the second page neither skips nor duplicates entries, plus a test for a malformed cursor.
InMemoryJobInventory.listinterprets the wirecursoras a non-negative integer offset into the principal's sorted job list atlib/src/main/kotlin/dev/arcp/runtime/JobInventory.kt:47(cursor?.toIntOrNull()?.coerceAtLeast(0) ?: 0), and thenextCursorit returns atlib/src/main/kotlin/dev/arcp/runtime/JobInventory.kt:56is juststart + page.size. Because the visible list is sorted bycreatedAtand thenjobIdatlib/src/main/kotlin/dev/arcp/runtime/JobInventory.kt:54, any new job recorded between twolistcalls shifts the indices that came after it, so the second page either repeats or skips entries depending on where the new record lands. A malformed cursor — anything not parsable as an int — silently restarts at zero rather than failing withINVALID_ARGUMENT, which both hides bugs and lets a client unknowingly re-walk the same prefix forever.Fix prompt: Encode the cursor as a stable opaque token tied to the sort key, for example a base64-encoded
(createdAt, jobId)tuple of the last entry returned, and resume by skipping past that key. Validate the cursor on input and throwARCPException.InvalidArgument("cursor")(which the dispatch path will then translate to a correlatedNack) when the value cannot be decoded. Add tests inlib/src/test/kotlin/dev/arcp/runtime/ListJobsHandlerTest.ktthat record a job between twolistcalls and assert the second page neither skips nor duplicates entries, plus a test for a malformed cursor.