@matt82198/aesop 0.1.0-beta.4 → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +187 -5
- package/LICENSE +66 -21
- package/README.md +143 -23
- package/aesop.config.example.json +14 -1
- package/bin/CLAUDE.md +42 -5
- package/bin/cli.js +479 -95
- package/daemons/CLAUDE.md +5 -2
- package/daemons/backup-fleet.sh +209 -51
- package/daemons/run-watchdog.sh +211 -62
- package/dash/dash-extra.mjs +73 -4
- package/dash/watchdog-gui.sh +41 -35
- package/docs/ARCHITECTURE.md +296 -0
- package/docs/CONCEPTS.md +254 -0
- package/docs/CONFIGURE.md +256 -0
- package/docs/FIRST-WAVE.md +276 -0
- package/docs/INSTALL.md +224 -0
- package/docs/README.md +176 -27
- package/docs/RELEASING.md +159 -0
- package/docs/archive/README.md +3 -0
- package/docs/{spikes → archive/spikes}/tiered-cognition/README.md +1 -3
- package/docs/autonomous-swe.md +149 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/reproduce.md +121 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +51 -0
- package/hooks/install-waveguard.sh +68 -0
- package/hooks/pre-commit-waveguard.sh +26 -0
- package/hooks/pre-push-policy.sh +253 -15
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/CHARTER.md +76 -110
- package/monitor/CLAUDE.md +3 -3
- package/monitor/collect-signals.mjs +218 -20
- package/package.json +17 -6
- package/scan/CLAUDE.md +30 -0
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +1 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +41 -0
- package/state_store/projections.py +126 -0
- package/state_store/store.py +197 -0
- package/tools/CLAUDE.md +37 -149
- package/tools/alert_bridge.py +453 -0
- package/tools/bench_runner.py +438 -0
- package/tools/buildlog.py +4 -7
- package/tools/ci_merge_wait.py +308 -14
- package/tools/common.py +62 -0
- package/tools/cost_ceiling.py +191 -0
- package/tools/dash.js +102 -0
- package/tools/doctor.js +220 -0
- package/tools/fleet_ledger.py +189 -17
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/halt.py +172 -0
- package/tools/healthcheck.py +299 -0
- package/tools/heartbeat.py +4 -7
- package/tools/metrics_gate.py +8 -2
- package/tools/orchestrator_status.py +4 -9
- package/tools/reconcile.py +277 -0
- package/tools/rotate_logs.py +152 -62
- package/tools/scanner_selftest.py +28 -3
- package/tools/secret_scan.py +367 -91
- package/tools/self_stats.py +797 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/status.js +187 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_agent_inspector.py +289 -0
- package/tools/verify_dash.py +361 -542
- package/tools/verify_prboard.py +344 -0
- package/tools/verify_submit_encoding.py +12 -4
- package/tools/watch.js +49 -0
- package/ui/CLAUDE.md +59 -41
- package/ui/agents.py +337 -34
- package/ui/api/submit.py +44 -5
- package/ui/api/tracker.py +15 -7
- package/ui/collectors.py +294 -52
- package/ui/config.py +31 -2
- package/ui/cost.py +355 -0
- package/ui/csrf.py +9 -4
- package/ui/handler.py +450 -12
- package/ui/render.py +26 -6
- package/ui/sse.py +114 -13
- package/ui/wave_prs.py +230 -0
- package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
- package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/monitor/.signal-state.json +0 -3
- package/monitor/ACTIONS.log +0 -1
- package/monitor/BRIEF.md +0 -24
- package/monitor/SIGNALS.json +0 -54
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/templates/dashboard.html +0 -1202
- /package/docs/{spikes → archive/spikes}/tiered-cognition/ACTIVATION.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/DESIGN.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/FINDINGS.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/aesop-cognition.example.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/force-model-policy.merged.mjs +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/strip-tools-hook.mjs +0 -0
package/ui/collectors.py
CHANGED
|
@@ -233,7 +233,7 @@ def get_main_thread_messages():
|
|
|
233
233
|
return messages
|
|
234
234
|
|
|
235
235
|
newest = jsonl_files[0]
|
|
236
|
-
with open(newest, 'r', encoding='utf-8', errors='
|
|
236
|
+
with open(newest, 'r', encoding='utf-8', errors='replace') as f:
|
|
237
237
|
lines = f.readlines()
|
|
238
238
|
# Get last 30 lines to extract ~12 message turns
|
|
239
239
|
for line in lines[-30:]:
|
|
@@ -316,7 +316,27 @@ def get_alerts():
|
|
|
316
316
|
return alerts
|
|
317
317
|
|
|
318
318
|
def load_tracker():
|
|
319
|
-
"""Load tracker.json, return empty tracker if missing or corrupt.
|
|
319
|
+
"""Load tracker.json, return empty tracker if missing or corrupt.
|
|
320
|
+
|
|
321
|
+
Defect 2 fix: Check dirty flag. If render previously failed, the event log
|
|
322
|
+
and tracker.json may be out of sync. Re-render from projection to self-heal.
|
|
323
|
+
"""
|
|
324
|
+
# Check dirty flag (indicates previous render failure)
|
|
325
|
+
dirty_file = config.STATE_DIR / ".tracker-render-dirty"
|
|
326
|
+
if dirty_file.exists():
|
|
327
|
+
try:
|
|
328
|
+
print("[tracker] Detected previous render failure; recovering...", file=sys.stderr)
|
|
329
|
+
api = _tracker_api()
|
|
330
|
+
# Force re-render from projection
|
|
331
|
+
try:
|
|
332
|
+
save_tracker(api.project("tracker"))
|
|
333
|
+
dirty_file.unlink()
|
|
334
|
+
print("[tracker] Recovery render completed", file=sys.stderr)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
print(f"[tracker] Recovery render failed: {e}", file=sys.stderr)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
print(f"[tracker] Failed to recover from dirty flag: {e}", file=sys.stderr)
|
|
339
|
+
|
|
320
340
|
if not config.TRACKER_FILE.exists():
|
|
321
341
|
return {"version": 1, "items": []}
|
|
322
342
|
|
|
@@ -418,9 +438,95 @@ def get_tracker_items(status=None, priority=None):
|
|
|
418
438
|
|
|
419
439
|
return items
|
|
420
440
|
|
|
441
|
+
# --- Event-sourced tracker backing (state_store) -----------------------------
|
|
442
|
+
# The event log is the write-authority + audit trail; tracker.json is kept as
|
|
443
|
+
# the rendered export that reads/SSE/UI consume. Each mutation appends an event
|
|
444
|
+
# and re-renders the file (atomic os.replace via save_tracker), replacing the
|
|
445
|
+
# old load->mutate->save read-modify-write that raced under concurrent writers.
|
|
446
|
+
# The live read path (load_tracker / get_tracker_items / SSE snapshot) is
|
|
447
|
+
# unchanged: it still reads tracker.json, which every write keeps current.
|
|
448
|
+
|
|
449
|
+
def _tracker_api():
|
|
450
|
+
"""Return a StateAPI over state/tracker_events.db (lazy import; call-time paths)."""
|
|
451
|
+
try:
|
|
452
|
+
from state_store import StateAPI
|
|
453
|
+
except ImportError:
|
|
454
|
+
from pathlib import Path as _Path
|
|
455
|
+
root = str(_Path(__file__).resolve().parents[1])
|
|
456
|
+
if root not in sys.path:
|
|
457
|
+
sys.path.insert(0, root)
|
|
458
|
+
from state_store import StateAPI
|
|
459
|
+
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
460
|
+
return StateAPI(str(config.STATE_DIR / "tracker_events.db"))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _ensure_tracker_migrated(api):
|
|
464
|
+
"""Backfill the event log from the existing tracker.json once (idempotent).
|
|
465
|
+
|
|
466
|
+
Defect 3 fix: Guard migration with a marker event (migration_started with
|
|
467
|
+
version=1). The first caller to append this marker wins and performs the
|
|
468
|
+
backfill. Subsequent callers see the marker and skip the backfill. This
|
|
469
|
+
prevents concurrent callers from polluting the audit log with duplicate
|
|
470
|
+
item_created events.
|
|
471
|
+
"""
|
|
472
|
+
events = api.get("tracker")
|
|
473
|
+
|
|
474
|
+
# Check for migration marker (first event of type "migration_started")
|
|
475
|
+
has_migration_marker = any(e.get("type") == "migration_started" and
|
|
476
|
+
e.get("payload", {}).get("version") == 1
|
|
477
|
+
for e in events)
|
|
478
|
+
if has_migration_marker:
|
|
479
|
+
# Migration already completed (or in progress); skip backfill
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
# Append migration marker first to atomically claim the migration
|
|
483
|
+
try:
|
|
484
|
+
api.append("tracker", "migration_started", {"version": 1}, "system")
|
|
485
|
+
except Exception as e:
|
|
486
|
+
print(f"[tracker] Failed to append migration marker: {e}", file=sys.stderr)
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
# Now safe to backfill (other callers will see marker and skip)
|
|
490
|
+
if config.TRACKER_FILE.exists():
|
|
491
|
+
try:
|
|
492
|
+
data = json.loads(config.TRACKER_FILE.read_text(encoding='utf-8'))
|
|
493
|
+
except Exception:
|
|
494
|
+
data = {"items": []}
|
|
495
|
+
for item in data.get("items", []):
|
|
496
|
+
if isinstance(item, dict) and item.get("id"):
|
|
497
|
+
api.append("tracker", "item_created", item, "migration")
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _tracker_items_by_id(api):
|
|
501
|
+
return {it["id"]: it for it in api.project("tracker")["items"]}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _render_tracker(api):
|
|
505
|
+
"""Materialize the projection back to tracker.json (atomic).
|
|
506
|
+
|
|
507
|
+
Defect 2 fix: Wrap render in try/except. On failure, log loudly and mark
|
|
508
|
+
a dirty flag so the next read triggers a recovery re-render. This ensures
|
|
509
|
+
that if the event log has events but tracker.json is stale, the next read
|
|
510
|
+
self-heals by detecting the mismatch and re-rendering.
|
|
511
|
+
"""
|
|
512
|
+
try:
|
|
513
|
+
save_tracker(api.project("tracker"))
|
|
514
|
+
except Exception as e:
|
|
515
|
+
print(f"[tracker] CRITICAL: Failed to render tracker after event append: {e}",
|
|
516
|
+
file=sys.stderr)
|
|
517
|
+
# Mark dirty flag for recovery on next read
|
|
518
|
+
dirty_file = config.STATE_DIR / ".tracker-render-dirty"
|
|
519
|
+
try:
|
|
520
|
+
dirty_file.write_text(str(time()), encoding='utf-8')
|
|
521
|
+
except Exception as e2:
|
|
522
|
+
print(f"[tracker] Failed to write dirty flag: {e2}", file=sys.stderr)
|
|
523
|
+
raise
|
|
524
|
+
|
|
525
|
+
|
|
421
526
|
def create_tracker_item(data):
|
|
422
|
-
"""Create a new tracker item."""
|
|
423
|
-
|
|
527
|
+
"""Create a new tracker item (event-sourced; tracker.json re-rendered)."""
|
|
528
|
+
api = _tracker_api()
|
|
529
|
+
_ensure_tracker_migrated(api)
|
|
424
530
|
|
|
425
531
|
item = {
|
|
426
532
|
"id": secrets.token_hex(6),
|
|
@@ -436,39 +542,43 @@ def create_tracker_item(data):
|
|
|
436
542
|
"completed_at": None
|
|
437
543
|
}
|
|
438
544
|
|
|
439
|
-
tracker["
|
|
440
|
-
|
|
545
|
+
api.append("tracker", "item_created", item, item["source"])
|
|
546
|
+
_render_tracker(api)
|
|
441
547
|
return item
|
|
442
548
|
|
|
443
549
|
def update_tracker_item(item_id, update_data):
|
|
444
|
-
"""Update a tracker item by id."""
|
|
445
|
-
|
|
550
|
+
"""Update a tracker item by id (event-sourced)."""
|
|
551
|
+
api = _tracker_api()
|
|
552
|
+
_ensure_tracker_migrated(api)
|
|
446
553
|
|
|
447
|
-
|
|
448
|
-
if not
|
|
554
|
+
current = _tracker_items_by_id(api)
|
|
555
|
+
if item_id not in current:
|
|
449
556
|
raise Exception(f"404 Item not found: {item_id}")
|
|
450
557
|
|
|
558
|
+
patch = {"id": item_id}
|
|
451
559
|
for key in ["status", "lane", "priority", "notes", "pr_link", "tags"]:
|
|
452
560
|
if key in update_data:
|
|
453
|
-
|
|
561
|
+
patch[key] = update_data[key]
|
|
454
562
|
|
|
455
|
-
if update_data.get("status") == "done" and not
|
|
456
|
-
|
|
563
|
+
if update_data.get("status") == "done" and not current[item_id].get("completed_at"):
|
|
564
|
+
patch["completed_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
457
565
|
|
|
458
|
-
|
|
459
|
-
|
|
566
|
+
api.append("tracker", "item_updated", patch, "api")
|
|
567
|
+
_render_tracker(api)
|
|
568
|
+
return _tracker_items_by_id(api)[item_id]
|
|
460
569
|
|
|
461
570
|
def delete_tracker_item(item_id):
|
|
462
|
-
"""Soft-delete a tracker item (mark as archived)."""
|
|
463
|
-
|
|
571
|
+
"""Soft-delete a tracker item (mark as archived; event-sourced)."""
|
|
572
|
+
api = _tracker_api()
|
|
573
|
+
_ensure_tracker_migrated(api)
|
|
464
574
|
|
|
465
|
-
|
|
466
|
-
if not
|
|
575
|
+
current = _tracker_items_by_id(api)
|
|
576
|
+
if item_id not in current:
|
|
467
577
|
raise Exception(f"404 Item not found: {item_id}")
|
|
468
578
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return
|
|
579
|
+
api.append("tracker", "item_archived", {"id": item_id}, "api")
|
|
580
|
+
_render_tracker(api)
|
|
581
|
+
return _tracker_items_by_id(api)[item_id]
|
|
472
582
|
|
|
473
583
|
def _snapshot_data():
|
|
474
584
|
"""Everything the 'data' SSE section covers (header, repos, events, alerts, messages)."""
|
|
@@ -529,28 +639,155 @@ def _snapshot_orchestrator_status():
|
|
|
529
639
|
print(f"[status] Snapshot error: {e}", file=sys.stderr)
|
|
530
640
|
return {"orchestrators": []}
|
|
531
641
|
|
|
642
|
+
def _recover_stranded_inbox_files():
|
|
643
|
+
"""Defect 3 fix: Recovery sweep for .tracker-inbox.jsonl.processing-* files.
|
|
644
|
+
|
|
645
|
+
If drain_tracker_inbox crashes mid-process, it leaves a .processing-* file.
|
|
646
|
+
This sweep re-ingests those files on the next drain call, preventing silent
|
|
647
|
+
data loss. Returns list of items recovered.
|
|
648
|
+
"""
|
|
649
|
+
recovered = []
|
|
650
|
+
try:
|
|
651
|
+
for processing_file in sorted(config.STATE_DIR.glob(".tracker-inbox.jsonl.processing-*")):
|
|
652
|
+
if not processing_file.exists():
|
|
653
|
+
continue
|
|
654
|
+
try:
|
|
655
|
+
content = processing_file.read_text(encoding='utf-8')
|
|
656
|
+
if not content.strip():
|
|
657
|
+
try:
|
|
658
|
+
processing_file.unlink()
|
|
659
|
+
except Exception:
|
|
660
|
+
pass
|
|
661
|
+
continue
|
|
662
|
+
|
|
663
|
+
# Build dedup hash set from both tracker.json and event store
|
|
664
|
+
existing_hashes = set()
|
|
665
|
+
|
|
666
|
+
# Check rendered tracker.json
|
|
667
|
+
tracker_json = load_tracker()
|
|
668
|
+
for item in tracker_json.get("items", []):
|
|
669
|
+
source = item.get("source", "")
|
|
670
|
+
title = item.get("title", "")
|
|
671
|
+
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
672
|
+
existing_hashes.add(h)
|
|
673
|
+
|
|
674
|
+
# Check event store projection
|
|
675
|
+
try:
|
|
676
|
+
api = _tracker_api()
|
|
677
|
+
projected = api.project("tracker")
|
|
678
|
+
for item in projected.get("items", []):
|
|
679
|
+
source = item.get("source", "")
|
|
680
|
+
title = item.get("title", "")
|
|
681
|
+
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
682
|
+
existing_hashes.add(h)
|
|
683
|
+
except Exception as e:
|
|
684
|
+
print(f"[inbox] Recovery: failed to project tracker state: {e}", file=sys.stderr)
|
|
685
|
+
|
|
686
|
+
# Reprocess lines from the recovered file
|
|
687
|
+
lines = content.strip().splitlines()
|
|
688
|
+
for line in lines:
|
|
689
|
+
line = line.strip()
|
|
690
|
+
if not line:
|
|
691
|
+
continue
|
|
692
|
+
try:
|
|
693
|
+
entry = json.loads(line)
|
|
694
|
+
if not isinstance(entry, dict):
|
|
695
|
+
continue
|
|
696
|
+
source = entry.get("source", "")
|
|
697
|
+
title = entry.get("title", "")
|
|
698
|
+
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
699
|
+
|
|
700
|
+
if h not in existing_hashes:
|
|
701
|
+
item = create_tracker_item(entry)
|
|
702
|
+
recovered.append(item)
|
|
703
|
+
existing_hashes.add(h)
|
|
704
|
+
except (json.JSONDecodeError, Exception):
|
|
705
|
+
pass
|
|
706
|
+
|
|
707
|
+
# Only delete after successful re-ingest
|
|
708
|
+
try:
|
|
709
|
+
processing_file.unlink()
|
|
710
|
+
except Exception as e:
|
|
711
|
+
print(f"[inbox] Recovery: failed to unlink {processing_file}: {e}", file=sys.stderr)
|
|
712
|
+
|
|
713
|
+
except Exception as e:
|
|
714
|
+
print(f"[inbox] Recovery sweep error on {processing_file}: {e}", file=sys.stderr)
|
|
715
|
+
except Exception as e:
|
|
716
|
+
print(f"[inbox] Recovery sweep failed: {e}", file=sys.stderr)
|
|
717
|
+
|
|
718
|
+
return recovered
|
|
719
|
+
|
|
720
|
+
|
|
532
721
|
def drain_tracker_inbox():
|
|
533
|
-
"""Drain .tracker-inbox.jsonl, create items idempotently.
|
|
722
|
+
"""Drain .tracker-inbox.jsonl, create items idempotently.
|
|
723
|
+
|
|
724
|
+
Defect 1 fix: Atomically rename inbox file to unique processing name FIRST
|
|
725
|
+
to ensure only one caller processes it under concurrent access. Strengthen
|
|
726
|
+
dedup to check both tracker.json AND api.project("tracker") so items
|
|
727
|
+
in the event store but not yet rendered are also excluded.
|
|
728
|
+
|
|
729
|
+
Defect 3 fix: Before processing the current inbox, perform a recovery sweep
|
|
730
|
+
to re-ingest leftover .tracker-inbox.jsonl.processing-* files from any
|
|
731
|
+
previous crashes. This prevents silent data loss if a crash happens
|
|
732
|
+
mid-drain: stranded files are recovered on the next drain call.
|
|
733
|
+
"""
|
|
534
734
|
inbox_file = config.STATE_DIR / ".tracker-inbox.jsonl"
|
|
735
|
+
|
|
736
|
+
# Defect 3: Recovery sweep for stranded .processing-* files from prior crashes.
|
|
737
|
+
created = _recover_stranded_inbox_files()
|
|
738
|
+
|
|
739
|
+
# Now process the current inbox if it exists
|
|
535
740
|
if not inbox_file.exists():
|
|
536
|
-
return
|
|
537
|
-
|
|
538
|
-
|
|
741
|
+
return created
|
|
742
|
+
|
|
743
|
+
# Defect 1: Atomically rename inbox to unique processing name first.
|
|
744
|
+
# This ensures only one caller wins; others see no file.
|
|
745
|
+
processing_file = inbox_file.with_name(
|
|
746
|
+
f".tracker-inbox.jsonl.processing-{secrets.token_hex(8)}"
|
|
747
|
+
)
|
|
748
|
+
try:
|
|
749
|
+
os.replace(str(inbox_file), str(processing_file))
|
|
750
|
+
except FileNotFoundError:
|
|
751
|
+
# Another caller already renamed it; nothing to process
|
|
752
|
+
return created
|
|
753
|
+
except Exception as e:
|
|
754
|
+
print(f"[inbox] Failed to rename inbox: {e}", file=sys.stderr)
|
|
755
|
+
return created
|
|
756
|
+
|
|
539
757
|
try:
|
|
540
|
-
content =
|
|
758
|
+
content = processing_file.read_text(encoding='utf-8')
|
|
541
759
|
if not content.strip():
|
|
542
|
-
|
|
543
|
-
return
|
|
544
|
-
|
|
760
|
+
processing_file.unlink()
|
|
761
|
+
return created
|
|
762
|
+
|
|
545
763
|
lines = content.strip().splitlines()
|
|
546
|
-
|
|
764
|
+
|
|
765
|
+
# Defect 1: Build dedup hash set from both tracker.json AND event store projection.
|
|
766
|
+
# This catches items in the event store that haven't been rendered to tracker.json yet.
|
|
547
767
|
existing_hashes = set()
|
|
548
|
-
|
|
768
|
+
|
|
769
|
+
# Check rendered tracker.json
|
|
770
|
+
tracker_json = load_tracker()
|
|
771
|
+
for item in tracker_json.get("items", []):
|
|
549
772
|
source = item.get("source", "")
|
|
550
773
|
title = item.get("title", "")
|
|
551
774
|
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
552
775
|
existing_hashes.add(h)
|
|
553
|
-
|
|
776
|
+
|
|
777
|
+
# Also check event store projection (catches items in DB but not yet rendered)
|
|
778
|
+
try:
|
|
779
|
+
api = _tracker_api()
|
|
780
|
+
projected = api.project("tracker")
|
|
781
|
+
for item in projected.get("items", []):
|
|
782
|
+
source = item.get("source", "")
|
|
783
|
+
title = item.get("title", "")
|
|
784
|
+
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
785
|
+
existing_hashes.add(h)
|
|
786
|
+
except Exception as e:
|
|
787
|
+
print(f"[inbox] Failed to project tracker state: {e}", file=sys.stderr)
|
|
788
|
+
# Fall back to tracker.json only if projection fails
|
|
789
|
+
pass
|
|
790
|
+
|
|
554
791
|
rejects = []
|
|
555
792
|
for line in lines:
|
|
556
793
|
line = line.strip()
|
|
@@ -558,29 +795,34 @@ def drain_tracker_inbox():
|
|
|
558
795
|
continue
|
|
559
796
|
try:
|
|
560
797
|
entry = json.loads(line)
|
|
561
|
-
if not isinstance(entry, dict):
|
|
562
|
-
rejects.append(line)
|
|
563
|
-
continue
|
|
564
|
-
|
|
565
|
-
source = entry.get("source", "")
|
|
566
|
-
title = entry.get("title", "")
|
|
567
|
-
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
568
|
-
|
|
569
|
-
if h not in existing_hashes:
|
|
570
|
-
item = create_tracker_item(entry)
|
|
571
|
-
created.append(item)
|
|
572
|
-
existing_hashes.add(h)
|
|
573
798
|
except json.JSONDecodeError:
|
|
574
799
|
rejects.append(line)
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
800
|
+
continue
|
|
801
|
+
|
|
802
|
+
if not isinstance(entry, dict):
|
|
803
|
+
rejects.append(line)
|
|
804
|
+
continue
|
|
805
|
+
|
|
806
|
+
source = entry.get("source", "")
|
|
807
|
+
title = entry.get("title", "")
|
|
808
|
+
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
809
|
+
|
|
810
|
+
if h not in existing_hashes:
|
|
811
|
+
# create_tracker_item can raise real errors (not malformed JSON)
|
|
812
|
+
# let those bubble up rather than silently adding to rejects
|
|
813
|
+
item = create_tracker_item(entry)
|
|
814
|
+
created.append(item)
|
|
815
|
+
existing_hashes.add(h)
|
|
816
|
+
|
|
578
817
|
if rejects:
|
|
579
818
|
rejects_file = inbox_file.with_name(".tracker-inbox.rejects")
|
|
580
819
|
rejects_file.write_text("\n".join(rejects) + "\n", encoding='utf-8')
|
|
581
|
-
|
|
582
|
-
|
|
820
|
+
|
|
821
|
+
# Only delete the processing file after successful completion.
|
|
822
|
+
# If an exception occurs, the file is left behind for recovery.
|
|
823
|
+
processing_file.unlink()
|
|
583
824
|
except Exception as e:
|
|
584
|
-
print(f"[inbox] Drain error: {e}", file=sys.stderr)
|
|
585
|
-
|
|
825
|
+
print(f"[inbox] Drain error: {e}; processing file {processing_file.name} left for recovery", file=sys.stderr)
|
|
826
|
+
# Don't delete the file on error; let recovery sweep handle it next time
|
|
827
|
+
|
|
586
828
|
return created
|
package/ui/config.py
CHANGED
|
@@ -28,13 +28,34 @@ def reload():
|
|
|
28
28
|
global WATCHDOG_HEARTBEAT, MONITOR_HEARTBEAT, REPOS_JSON, BACKUP_LOG
|
|
29
29
|
global ALERTS_LOG, INBOX_FILE, AUDIT_BACKLOG_FILE
|
|
30
30
|
global UI_SESSION_TOKEN_FILE, TRACKER_FILE, ORCH_STATUS_FILE
|
|
31
|
+
global WEB_DIST, LEDGER_FILE
|
|
31
32
|
global COLLECTOR_INTERVAL, SSE_KEEPALIVE_SECONDS, SSE_MAX_CLIENTS, SSE_QUEUE_MAXSIZE, SSE_WRITE_TIMEOUT
|
|
32
33
|
|
|
33
34
|
# PORT: env PORT > default 8770
|
|
34
35
|
PORT = int(os.getenv("PORT", "8770"))
|
|
35
36
|
|
|
36
|
-
# Determine AESOP_ROOT
|
|
37
|
-
|
|
37
|
+
# Determine AESOP_ROOT with fallback tiers (matching daemons/run-watchdog.sh pattern):
|
|
38
|
+
# (1) AESOP_ROOT env var if set
|
|
39
|
+
# (2) Derive from file location: Path(__file__).resolve().parents[1]
|
|
40
|
+
# (3) Load config from derived location; if it has aesop_root, use that
|
|
41
|
+
env_root = os.getenv("AESOP_ROOT")
|
|
42
|
+
if env_root:
|
|
43
|
+
AESOP_ROOT = Path(env_root)
|
|
44
|
+
else:
|
|
45
|
+
# Derive from file location (matches daemons/run-watchdog.sh pattern)
|
|
46
|
+
AESOP_ROOT = Path(__file__).resolve().parents[1]
|
|
47
|
+
|
|
48
|
+
# Check if derived location's config has aesop_root key
|
|
49
|
+
derived_config_file = AESOP_ROOT / "aesop.config.json"
|
|
50
|
+
if derived_config_file.exists():
|
|
51
|
+
try:
|
|
52
|
+
with open(derived_config_file) as f:
|
|
53
|
+
derived_config = json.load(f)
|
|
54
|
+
if "aesop_root" in derived_config:
|
|
55
|
+
AESOP_ROOT = Path(derived_config["aesop_root"])
|
|
56
|
+
except Exception:
|
|
57
|
+
# Silently ignore config errors here; will attempt full load below
|
|
58
|
+
pass
|
|
38
59
|
|
|
39
60
|
# Try to load config file for additional settings
|
|
40
61
|
CONFIG_FILE = AESOP_ROOT / "aesop.config.json"
|
|
@@ -75,6 +96,12 @@ def reload():
|
|
|
75
96
|
TRACKER_FILE = STATE_DIR / "tracker.json"
|
|
76
97
|
ORCH_STATUS_FILE = STATE_DIR / "orchestrator-status.json"
|
|
77
98
|
|
|
99
|
+
# Wave-14 dashboard rewrite (plan D3): built frontend + cost ledger paths.
|
|
100
|
+
# WEB_DIST: the committed Vite build output served at / and /assets/*.
|
|
101
|
+
WEB_DIST = AESOP_ROOT / "ui" / "web" / "dist"
|
|
102
|
+
# LEDGER_FILE: outcomes ledger parsed by ui/cost.py; sse.py mtime-gates on it.
|
|
103
|
+
LEDGER_FILE = STATE_DIR / "ledger" / "OUTCOMES-LEDGER.md"
|
|
104
|
+
|
|
78
105
|
# Collector and SSE configuration
|
|
79
106
|
COLLECTOR_INTERVAL = float(os.getenv("AESOP_UI_COLLECT_INTERVAL", "1.0"))
|
|
80
107
|
SSE_KEEPALIVE_SECONDS = 15
|
|
@@ -104,6 +131,8 @@ AUDIT_BACKLOG_FILE = AESOP_ROOT / "AUDIT-BACKLOG.md"
|
|
|
104
131
|
UI_SESSION_TOKEN_FILE = STATE_DIR / ".ui-session-token"
|
|
105
132
|
TRACKER_FILE = STATE_DIR / "tracker.json"
|
|
106
133
|
ORCH_STATUS_FILE = STATE_DIR / "orchestrator-status.json"
|
|
134
|
+
WEB_DIST = AESOP_ROOT / "ui" / "web" / "dist"
|
|
135
|
+
LEDGER_FILE = STATE_DIR / "ledger" / "OUTCOMES-LEDGER.md"
|
|
107
136
|
COLLECTOR_INTERVAL = 1.0
|
|
108
137
|
SSE_KEEPALIVE_SECONDS = 15
|
|
109
138
|
SSE_MAX_CLIENTS = 100
|