diff --git a/project/ticket-008/README.md b/project/ticket-008/README.md new file mode 100644 index 0000000..72161df --- /dev/null +++ b/project/ticket-008/README.md @@ -0,0 +1,28 @@ +# Ticket 008: Reduce Process URI estimation overhead + +- **ID**: ticket-008 +- **Owner**: unresolved:human +- **Status**: IN_PROGRESS +- **Workflow state**: EDIT +- **Created**: 2026-08-25 + +## Goal and scope + +Reduce the observer effect of Process URI estimation while preserving bounded, +append-only evidence. Use a one-second default sample interval and read only +the tail of the event log when extending its hash chain. + +## Acceptance criteria + +- [x] AC-01: Scope is approved by the user's request to measure and reduce + Subactor resource consumption. +- [ ] AC-02: Command and PID observation default to one sample per second. +- [ ] AC-03: Appending an event does not scan the full historical JSONL file. +- [ ] AC-04: Tests prove the new defaults and bounded tail read. +- [ ] AC-05: Pytest and governance checks pass. + +## Participants + +- Human participant: user:tom; authorization is recorded by the originating + request and no synthesized user file was created. +- Agent participant: [ai-codex.md](ai-codex.md) diff --git a/project/ticket-008/ai-codex-logs.txt b/project/ticket-008/ai-codex-logs.txt new file mode 100644 index 0000000..e69de29 diff --git a/project/ticket-008/ai-codex.md b/project/ticket-008/ai-codex.md new file mode 100644 index 0000000..290a38e --- /dev/null +++ b/project/ticket-008/ai-codex.md @@ -0,0 +1,33 @@ +--- +participant-id: agent:codex +participant: codex +role: agent +ticket: ticket-008 +--- +# Participant: codex (AI agent) + +## Understanding + +The 10 Hz default consumed about 1.62 CPU seconds during a six-second sample; +the same workload at 1 Hz consumed about 0.36 CPU seconds, a reduction near +78 percent. Event append also rereads all prior evidence and grows linearly. + +## Execution plan + +1. Change observer defaults from 10 Hz to 1 Hz. +2. Resolve the prior event from a bounded tail read. +3. Add regression tests for both resource guardrails. +4. Run pytest and governance, then publish through validator-agent. + +## Actual changes + +- Initialized the bounded ticket and recorded SESSION_EXECUTION_AUTHORIZATION + from the request to execute this work. + +## Blockers + +- None inside the recorded intent; proceed without a second confirmation. +- New authority remains required for destructive action, secret access, new + external coordination or material objective expansion. Protected delivery + may be invoked without another prompt when publication is in scope; its + exact-head trusted approval remains independent evidence. diff --git a/project/ticket-008/changelog.md b/project/ticket-008/changelog.md new file mode 100644 index 0000000..9161db1 --- /dev/null +++ b/project/ticket-008/changelog.md @@ -0,0 +1,6 @@ +# Ticket Changelog (ticket-008) + +## [0.1.0] - 2026-08-25 + +- Initial governance scaffold created. +- No human participant identity or content was generated. diff --git a/project/ticket-008/intent.json b/project/ticket-008/intent.json new file mode 100644 index 0000000..4f9f42d --- /dev/null +++ b/project/ticket-008/intent.json @@ -0,0 +1,71 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-008", + "summary": "Reduce Process URI estimation overhead", + "workstream": "application", + "classification": { + "kind": "SERVICE", + "priority": "P2", + "origin": "health" + }, + "allowedPaths": ["project/ticket-008/**", "TODO.md", "project/TICKETS.md", "src/**", "tests/**"], + "delivery": { + "acceptedBaseSha": "8dd7bc814b0fa987e968524745913771910560c7", + "targetBranch": "main", + "outcome": "Process URI estimation samples at 1 Hz by default and extends its event hash chain from a bounded tail read.", + "nonGoals": [ + "Change Process URI authorization or scheduling authority.", + "Change the persisted sample or event schema." + ], + "complexity": "S", + "estimatedMinutes": 25, + "budgets": { + "maxImplementationFiles": 5, + "maxAffectedComponents": 1, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Keep estimation advisory and append-only; reduce sampling frequency and resolve only the final JSONL record with positional reads.", + "components": [ + { + "name": "estimation-runtime", + "paths": ["src/**", "tests/**"] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Restore the 0.1 second defaults and sequential last-event scan; stored JSONL remains compatible." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-02", + "commands": ["PYTHONPATH=src python -m pytest"], + "evidence": "Signature regression test asserts one-second defaults." + }, + { + "criterion": "AC-03", + "commands": ["PYTHONPATH=src python -m pytest"], + "evidence": "Tail-read regression test limits the final append to one 4096-byte pread." + }, + { + "criterion": "AC-05", + "commands": ["PYTHONPATH=src ./project/governance-check.sh"], + "evidence": "Governance report must return GOV-PASS." + } + ] + }, + "forbiddenPaths": ["project/ticket-*/user-*.md"], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null +} diff --git a/project/ticket-008/preprompt.md b/project/ticket-008/preprompt.md new file mode 100644 index 0000000..b2b5645 --- /dev/null +++ b/project/ticket-008/preprompt.md @@ -0,0 +1,15 @@ +# Ticket preprompt + +- **Task ID**: ticket-008 +- **Task title**: Reduce Process URI estimation overhead +- **Created**: 2026-08-25T17:11:14Z + +Keep executable implementation outside this governance/evidence directory. +Read a human-owned user-*.md file only when one exists. +The request to execute this work creates SESSION_EXECUTION_AUTHORIZATION; +proceed within the recorded intent without a redundant confirmation prompt. +Require new authority for destructive action, secrets, external coordination, +or material objective expansion. When publication is in scope, session +authorization permits the declared protected delivery process and its merge +after exact-head trusted approval without another prompt. Session prose is +never approval evidence and the agent must not merge directly. diff --git a/src/estimation/cli.py b/src/estimation/cli.py index 65e4449..9851461 100644 --- a/src/estimation/cli.py +++ b/src/estimation/cli.py @@ -24,7 +24,7 @@ def _context(parser: argparse.ArgumentParser) -> None: parser.add_argument("--correlation-id", default=os.getenv("SUBACTOR_CORRELATION_ID")) parser.add_argument("--store", default=DEFAULT_STORE) parser.add_argument("--events", default=DEFAULT_EVENTS) - parser.add_argument("--interval", type=float, default=0.1) + parser.add_argument("--interval", type=float, default=1.0) def _parser() -> argparse.ArgumentParser: diff --git a/src/estimation/monitor.py b/src/estimation/monitor.py index 1db4c8a..6867f98 100644 --- a/src/estimation/monitor.py +++ b/src/estimation/monitor.py @@ -106,7 +106,7 @@ def measure_command( process_key: str | None = None, ticket_id: str | None = None, correlation_id: str | None = None, - interval_seconds: float = 0.1, + interval_seconds: float = 1.0, ) -> Sample: argv = [str(item) for item in command] if not argv: @@ -157,7 +157,7 @@ def observe_pid( process_key: str | None = None, ticket_id: str | None = None, correlation_id: str | None = None, - interval_seconds: float = 0.1, + interval_seconds: float = 1.0, duration_seconds: float = 10.0, ) -> Sample: root = psutil.Process(int(pid)) diff --git a/src/estimation/store.py b/src/estimation/store.py index 2cdb525..3709a89 100644 --- a/src/estimation/store.py +++ b/src/estimation/store.py @@ -3,6 +3,7 @@ from contextlib import contextmanager import hashlib import json +import os from pathlib import Path from typing import Any, Iterator @@ -42,14 +43,21 @@ def _append_line(path: Path, payload: dict[str, Any]) -> None: def _last_event(handle: Any) -> tuple[int, str]: - handle.seek(0) - last: dict[str, Any] | None = None - for raw_line in handle: - line = raw_line.strip() - if line: - last = json.loads(line) - if last is None: + handle.flush() + offset = os.fstat(handle.fileno()).st_size + suffix = b"" + while offset > 0: + size = min(4096, offset) + offset -= size + suffix = os.pread(handle.fileno(), size, offset) + suffix + stripped = suffix.rstrip() + separator = max(stripped.rfind(b"\n"), stripped.rfind(b"\r")) + if separator >= 0: + last = json.loads(stripped[separator + 1 :].decode("utf-8")) + return int(last["sequence"]), str(last["eventHash"]) + if not suffix.strip(): return 0, ZERO_HASH + last = json.loads(suffix.strip().decode("utf-8")) return int(last["sequence"]), str(last["eventHash"]) diff --git a/tests/test_estimation.py b/tests/test_estimation.py index fd26306..c03e604 100644 --- a/tests/test_estimation.py +++ b/tests/test_estimation.py @@ -1,5 +1,6 @@ from __future__ import annotations +import inspect import json import os from pathlib import Path @@ -10,6 +11,7 @@ from estimation.monitor import measure_command, observe_pid from estimation.stats import aggregate_samples, estimate_workload from estimation.store import append_sample, load_samples +import estimation.store as store_module def _sample(duration: float, memory: int, *, outcome: str = "succeeded"): @@ -72,6 +74,31 @@ def test_event_store_builds_a_hash_chain(tmp_path: Path) -> None: assert all(row["secretMaterialIncluded"] is False for row in rows) +def test_monitor_defaults_to_one_hertz() -> None: + assert inspect.signature(measure_command).parameters["interval_seconds"].default == 1.0 + assert inspect.signature(observe_pid).parameters["interval_seconds"].default == 1.0 + + +def test_event_append_reads_only_bounded_tail(tmp_path: Path, monkeypatch) -> None: + store = tmp_path / "samples.jsonl" + events = tmp_path / "events.jsonl" + for index in range(1000): + append_sample(_sample(float(index + 1), index + 1), store, events) + + original_pread = store_module.os.pread + bytes_read = 0 + + def measured_pread(fd: int, size: int, offset: int) -> bytes: + nonlocal bytes_read + chunk = original_pread(fd, size, offset) + bytes_read += len(chunk) + return chunk + + monkeypatch.setattr(store_module.os, "pread", measured_pread) + append_sample(_sample(1001.0, 1001), store, events) + assert bytes_read <= 4096 + + def test_report_and_workload_estimate_use_successful_p90() -> None: samples = [_sample(1.0, 100), _sample(2.0, 200), _sample(3.0, 300), _sample(20.0, 999, outcome="failed")] report = aggregate_samples(samples)