Skip to content

Commit f1004bc

Browse files
nficanocursoragent
andcommitted
fix: make event_seq session-scoped, not job-scoped (§8.3)
Key the monotonic event_seq counter by the submitter session_id so every job in a session shares one strictly-monotonic, gap-free stream. Two jobs in the same session no longer each start at 1 and collide in that session's replay window, restoring the resume gap-free guarantee. (#43) Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 87cf9dc commit f1004bc

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

lib/arcp/runtime/job_manager.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def initialize(runtime:, lease_manager:, subscription_manager:, event_log:, cloc
3333
@jobs = {} # job_id => JobRecord
3434
@order = [] # insertion order of job_ids (oldest first)
3535
@next_seq = 0 # monotonic counter assigned at submit
36-
@event_seq = Hash.new(0) # job_id => last emitted seq
36+
@event_seq = Hash.new(0) # submitter session_id => last emitted seq
3737
@idempotency = {} # [principal, key] => job_id
3838
@accepted = {} # job_id => [resolved, lease, credentials, accepted_at]
3939
@mutex = Mutex.new
@@ -237,10 +237,17 @@ def record_created_after?(record, threshold)
237237
def lookup(job_id) = @mutex.synchronize { @jobs[job_id] }
238238

239239
def publish_event(job_id, event)
240-
seq = @mutex.synchronize { @event_seq[job_id] += 1 }
240+
# Spec §8.3: event_seq is session-scoped, strictly monotonic, and
241+
# gap-free. Key the counter by the submitter session so every job in
242+
# a session shares one monotonic stream (two jobs no longer both
243+
# start at 1 and collide in that session's replay window).
244+
session_id, seq = @mutex.synchronize do
245+
sid = @jobs[job_id]&.submitter_session_id || ''
246+
[sid, @event_seq[sid] += 1]
247+
end
241248
env = Arcp::Envelope.build(
242249
type: Arcp::MessageTypes::JOB_EVENT,
243-
session_id: @mutex.synchronize { @jobs[job_id]&.submitter_session_id || '' },
250+
session_id: session_id,
244251
job_id: job_id, event_seq: seq, payload: event.to_h
245252
)
246253
@event_log.append(env.session_id, env)

spec/integration/audit_findings_2026_05_28_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,38 @@
4747
end
4848
end
4949

50+
describe 'event_seq is session-scoped across a session\'s jobs (#43)' do
51+
it 'assigns strictly increasing, gap-free seqs across two jobs in one session' do
52+
Sync do
53+
runtime = build_runtime(
54+
agents: { emitter: lambda { |ctx|
55+
3.times { |i| ctx.log(level: 'info', message: "m#{i}") }
56+
Async::Task.current.sleep(5)
57+
} }
58+
)
59+
client, server_task = open_pair(runtime)
60+
61+
client.submit_job(agent: 'emitter')
62+
client.submit_job(agent: 'emitter')
63+
Async::Task.current.sleep(0.1)
64+
65+
session_id = client.session.id
66+
seqs = runtime.event_log
67+
.replay(session_id)
68+
.select { |e| e.type == Arcp::MessageTypes::JOB_EVENT }
69+
.map(&:event_seq)
70+
71+
expect(seqs.size).to eq(6)
72+
expect(seqs.uniq).to eq(seqs) # no repeats across the two jobs
73+
expect(seqs).to eq(seqs.sort) # strictly monotonic
74+
expect(seqs).to eq((1..6).to_a) # gap-free from 1
75+
76+
client.close
77+
server_task.stop
78+
end
79+
end
80+
end
81+
5082
describe 'cancellation emits job.cancelled then job.error (#47)' do
5183
it 'acknowledges with job.cancelled before the CANCELLED job.error' do
5284
Sync do

0 commit comments

Comments
 (0)