Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion JSON_RPC_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@ watchpoints stop after the access; resuming does not suppress the next access.

No parameters. Requests that execution stop at the next instruction boundary.
When the request returns, no further CPU instruction is executed until a continue
or step request. Result:
or step request. If already stopped, pause is read-only: it preserves the last stop,
operation results, revision and breakpoint resume context. Result:

```json
{"paused":true,"general":{},"segments":{},"ip":0,"flags":0,
Expand Down
17 changes: 17 additions & 0 deletions docs/HANDOFF.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# Handoff - idempotent stopped pause, 2026-10-06

FINISHED local: execution.pause on a stopped machine preserves the complete
machine/control state, retained stop and breakpoint resume context. Continuing
still executes the stopped instruction once before the next breakpoint. Active
pause still cancels step/run_until and completes their operation without CPU
execution. New stopped-pause controls fail on the old handler.
FINISHED gates: 42 affected tests pass under CPython and PyPy; removing the
stopped-pause guard fails both new state-preservation controls, restored source
passes. Scoped review found no defects; paired old/new continuation confirms the
old handler retriggers the current breakpoint and the fixed handler reaches the
next breakpoint. PR73 epoch/API integrated at d9e0f159 after exact-head public CI.
Rebase onto that merged master is complete. WIP: final-head public CI and
pause integration. Parent pin stays fixed.

Earlier handoff notes below describe their historical slices.

# Handoff - keyboard input epochs, 2026-10-06

FINISHED local: each validated input.keyboard/keyboard.scancode batch advances
Expand Down
2 changes: 2 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,8 @@ def handle_debug(request):
}

if method == 'execution.pause':
if control['paused']:
return {'paused': True, **rpc_registers()}
rpc_clear_run_until()
control['paused'] = True
control['step'] = False
Expand Down
42 changes: 42 additions & 0 deletions tests/test_execution_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
The original while-loop body is executed once per pump in a one-item loop.
"""
import ast
import copy
import base64
import hashlib
import json
Expand Down Expand Up @@ -84,6 +85,47 @@ def interrupt_bp(self):


class ExecutionRPCTests(unittest.TestCase):
def test_redundant_pause_preserves_stop_and_execution_breakpoint_resume(self):
machine = HeadlessMachine()
machine.load(b'\x90\x90')
first = machine.execution_bp(0x100)
next_breakpoint = machine.execution_bp(0x101)
operation = machine.rpc('execution.continue')
machine.pump()
self.assertEqual(machine.control['last_stop']['breakpoint_id'], first)
stopped = copy.deepcopy(machine.control)
registers = machine.rpc('state.get_registers')
for _ in range(2):
self.assertEqual(machine.rpc('execution.pause'), {'paused': True, **registers})
self.assertEqual(machine.control, stopped)
self.assertEqual(machine.rpc('execution.wait', operation_id=operation['operation_id'])['stop_reason'],
stopped['last_stop'])
machine.rpc('execution.continue')
machine.pump()
self.assertEqual(machine.state.GetIP(), 0x101)
machine.pump()
self.assertEqual(machine.control['last_stop']['breakpoint_id'], next_breakpoint)

def test_active_pause_cancels_step_and_run_until_without_executing(self):
for method, params in (('execution.step', {}), ('execution.run_until', {'predicate': {
'address': {'space': 'segmented', 'segment': 0x1000, 'offset': 0x102}}})):
with self.subTest(method=method):
machine = HeadlessMachine()
machine.load(b'\x90\x90\x90')
before = machine.rpc('state.get_registers')
operation = machine.rpc(method, **params)
machine.rpc('execution.pause')
self.assertEqual(machine.rpc('state.get_registers'), before)
self.assertTrue(machine.control['paused'])
self.assertFalse(machine.control['step'])
self.assertIsNone(machine.control['run_until_id'])
self.assertIsNone(machine.control['skip_breakpoint_id'])
self.assertIsNone(machine.control['active_operation'])
done = machine.rpc('execution.wait', operation_id=operation['operation_id'])
self.assertEqual(done['stop_reason']['kind'], 'pause')
self.assertEqual(done['stop_reason']['registers'], before)
self.assertEqual(machine.rpc('breakpoints.list')['breakpoints'], [])

def test_running_keyboard_enqueue_advances_epoch_without_completing_operation(self):
machine = HeadlessMachine()
machine.namespace['kb'] = keyboard.Keyboard()
Expand Down
14 changes: 14 additions & 0 deletions tests/test_snapshot_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@


class SnapshotContractTests(unittest.TestCase):
def test_redundant_pause_preserves_complete_machine_and_control(self):
state = self.cpu.GetState()
breakpoint = self.h.rpc('breakpoints.create', address={
'space': 'segmented', 'segment': state.GetCS(), 'offset': state.GetIP()})['breakpoint_id']
self.h.rpc('execution.continue')
self.h.pump()
self.assertEqual(self.h.control['last_stop']['breakpoint_id'], breakpoint)
before = capture_machine(self.cpu)
controls = copy.deepcopy(self.h.control)
for _ in range(2):
self.h.rpc('execution.pause')
self.assertEqual(capture_machine(self.cpu), before)
self.assertEqual(self.h.control, controls)

def test_keyboard_epochs_guard_complete_snapshot_state_and_rejected_batches(self):
self.h.namespace['kb'] = self.cpu._devices[1]
old = self.export()
Expand Down
Loading