Nipun Claude Opus 4.6 commited on
Commit
f683fc1
Β·
1 Parent(s): 25452d0

v0.21.1: Fix SSE timeout + add diagnostic logging

Browse files

- Disable proxy buffering (X-Accel-Buffering: no) so heartbeats
actually reach the client through HF Spaces nginx
- Increase uvicorn timeout-keep-alive to 300s
- Add 10-minute AbortSignal.timeout on frontend fetch
- Add server-side logging (Python logging) for post-L5 stages
- Add SSE log events with timing for Polish, Judge, Report
- Improve frontend error display: shows elapsed time, last active
layer, and human-readable message for network/timeout errors
- Bump version to v0.21.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (5) hide show
  1. CHANGELOG.md +6 -0
  2. Dockerfile +1 -1
  3. main.py +36 -4
  4. static/index.html +2 -2
  5. static/js/app.js +16 -2
CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
  # CRCS Hybrid Engine β€” Changelog
2
 
 
 
 
 
 
 
3
  ## v0.21.0 (2026-04-05)
4
  - **Architecture matches Companion Paper exactly** β€” two-phase lattice protocol:
5
  - **Phase 1** (single pass): L1β†’L2β†’L3βˆ₯L3Aβ†’L4β†’L5 β†’ select m* = argmax Score(m)
 
1
  # CRCS Hybrid Engine β€” Changelog
2
 
3
+ ## v0.21.1 (2026-04-06)
4
+ - **Fix SSE network error on HF Spaces** β€” heartbeat keepalive every 15s prevents proxy timeout
5
+ - **Disable proxy buffering** β€” `X-Accel-Buffering: no` header ensures heartbeats reach client immediately
6
+ - **Increase uvicorn keep-alive** β€” 300s timeout-keep-alive in Dockerfile
7
+ - **Frontend timeout** β€” 10-minute AbortSignal.timeout on fetch to prevent browser-side abort
8
+
9
  ## v0.21.0 (2026-04-05)
10
  - **Architecture matches Companion Paper exactly** β€” two-phase lattice protocol:
11
  - **Phase 1** (single pass): L1β†’L2β†’L3βˆ₯L3Aβ†’L4β†’L5 β†’ select m* = argmax Score(m)
Dockerfile CHANGED
@@ -18,4 +18,4 @@ RUN python3 -c "import json,subprocess,datetime; \
18
 
19
  EXPOSE 7860
20
 
21
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
18
 
19
  EXPOSE 7860
20
 
21
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860", "--timeout-keep-alive", "300"]
main.py CHANGED
@@ -9,11 +9,15 @@ Endpoints:
9
  import asyncio
10
  import datetime
11
  import json
 
12
  import os
13
  import time
14
  from pathlib import Path
15
  from concurrent.futures import ThreadPoolExecutor
16
 
 
 
 
17
  from dotenv import load_dotenv
18
  load_dotenv()
19
 
@@ -741,6 +745,8 @@ async def run_pipeline(request: Request):
741
 
742
  # ── Polish: add citations and formatting to stable m* ────────
743
  # This happens AFTER Phase 2 convergence β€” polish is presentation, not constraint
 
 
744
  yield send("status", {"msg": "Polishing stable m* with citations…"})
745
  yield send("layer", {"id": "L5P", "name": "Polish β€” Citations & Formatting", "status": "running", "model": MODEL_CONSTRAINT})
746
  try:
@@ -748,9 +754,12 @@ async def run_pipeline(request: Request):
748
  _executor,
749
  lambda: polish_mstar(client, best_answer, query, domain,
750
  all_evidence_text, all_sources, add))
 
 
751
  for e in log_entries:
752
  yield send("log", e)
753
  log_entries.clear()
 
754
  yield send("layer", {"id": "L5P", "status": "pass"})
755
  yield send("polished", {"answer": polished_answer,
756
  "sources": [{"title": s.get("title",""),
@@ -758,7 +767,8 @@ async def run_pipeline(request: Request):
758
  "year": s.get("year","n.d.")}
759
  for s in all_sources[:8]]})
760
  except Exception as e:
761
- yield send("log", {"level": "WARN", "msg": f"Polish failed: {e}", "time": ts()})
 
762
  yield send("layer", {"id": "L5P", "status": "fail"})
763
  polished_answer = best_answer # fallback to unpolished
764
 
@@ -786,7 +796,10 @@ async def run_pipeline(request: Request):
786
  # ── LLM Judge: Baseline vs CRCS ──────────────────────────────
787
  judge_result = None
788
  if baseline_answer:
 
 
789
  yield send("status", {"msg": "Running LLM judge comparison…"})
 
790
  yield send("layer", {"id": "JUDGE", "name": "LLM Judge β€” Baseline vs CRCS", "status": "running", "model": MODEL_CONSTRAINT})
791
  try:
792
  _dim_schema = {"type": "object", "properties": {
@@ -847,10 +860,14 @@ async def run_pipeline(request: Request):
847
 
848
  judge_result = _map_scores(raw_judge, a_is_baseline)
849
  judge_result["_a_is_baseline"] = a_is_baseline
 
 
 
850
  yield send("layer", {"id": "JUDGE", "status": "pass"})
851
  yield send("judge", judge_result)
852
  except Exception as e:
853
- yield send("log", {"level": "WARN", "msg": f"Judge failed: {e}", "time": ts()})
 
854
  yield send("layer", {"id": "JUDGE", "status": "fail"})
855
 
856
  # ── Provenance ────────────────────────────────────────────────
@@ -874,18 +891,25 @@ async def run_pipeline(request: Request):
874
 
875
  # ── Report generation ─────────────────────────────────────────
876
  report_id = None
 
 
877
  yield send("status", {"msg": "Generating report…"})
 
878
  try:
879
  from report import generate_report, save_temp
880
  report_md = await loop.run_in_executor(_executor, lambda: generate_report(
881
  client, MODEL_GENERATION, query, domain, weights,
882
  iterations, boundary, all_sources,
883
  baseline=baseline_answer))
 
 
 
884
  report_id = secrets.token_hex(8)
885
  _reports[report_id] = {"md": report_md, "query": query, "domain": domain}
886
  yield send("report", {"report_id": report_id, "length": len(report_md)})
887
  except Exception as e:
888
- yield send("log", {"level": "WARN", "msg": f"Report failed: {e}", "time": ts()})
 
889
 
890
  # ── L6: S-genuine review gate β€” BLOCKS until decision ────────
891
  review_required = sims["high_stakes"]
@@ -979,7 +1003,15 @@ async def run_pipeline(request: Request):
979
  # No event in `interval` seconds β€” send keepalive
980
  yield ": heartbeat\n\n"
981
 
982
- return StreamingResponse(heartbeat_wrap(event_stream()), media_type="text/event-stream")
 
 
 
 
 
 
 
 
983
 
984
 
985
  # ── Entry point ──────────────────────────────────────────────────────────────
 
9
  import asyncio
10
  import datetime
11
  import json
12
+ import logging
13
  import os
14
  import time
15
  from pathlib import Path
16
  from concurrent.futures import ThreadPoolExecutor
17
 
18
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
19
+ log = logging.getLogger("crcs")
20
+
21
  from dotenv import load_dotenv
22
  load_dotenv()
23
 
 
745
 
746
  # ── Polish: add citations and formatting to stable m* ────────
747
  # This happens AFTER Phase 2 convergence β€” polish is presentation, not constraint
748
+ log.info("POST-L5: starting Polish")
749
+ _t_polish = time.time()
750
  yield send("status", {"msg": "Polishing stable m* with citations…"})
751
  yield send("layer", {"id": "L5P", "name": "Polish β€” Citations & Formatting", "status": "running", "model": MODEL_CONSTRAINT})
752
  try:
 
754
  _executor,
755
  lambda: polish_mstar(client, best_answer, query, domain,
756
  all_evidence_text, all_sources, add))
757
+ _dt = time.time() - _t_polish
758
+ log.info(f"POST-L5: Polish done ({_dt:.1f}s)")
759
  for e in log_entries:
760
  yield send("log", e)
761
  log_entries.clear()
762
+ yield send("log", {"level": "INFO", "msg": f"Polish completed ({_dt:.1f}s)", "time": ts()})
763
  yield send("layer", {"id": "L5P", "status": "pass"})
764
  yield send("polished", {"answer": polished_answer,
765
  "sources": [{"title": s.get("title",""),
 
767
  "year": s.get("year","n.d.")}
768
  for s in all_sources[:8]]})
769
  except Exception as e:
770
+ log.error(f"POST-L5: Polish FAILED after {time.time()-_t_polish:.1f}s: {e}")
771
+ yield send("log", {"level": "ERROR", "msg": f"Polish failed ({time.time()-_t_polish:.1f}s): {e}", "time": ts()})
772
  yield send("layer", {"id": "L5P", "status": "fail"})
773
  polished_answer = best_answer # fallback to unpolished
774
 
 
796
  # ── LLM Judge: Baseline vs CRCS ──────────────────────────────
797
  judge_result = None
798
  if baseline_answer:
799
+ log.info("POST-L5: starting Judge")
800
+ _t_judge = time.time()
801
  yield send("status", {"msg": "Running LLM judge comparison…"})
802
+ yield send("log", {"level": "INFO", "msg": "Starting LLM Judge…", "time": ts()})
803
  yield send("layer", {"id": "JUDGE", "name": "LLM Judge β€” Baseline vs CRCS", "status": "running", "model": MODEL_CONSTRAINT})
804
  try:
805
  _dim_schema = {"type": "object", "properties": {
 
860
 
861
  judge_result = _map_scores(raw_judge, a_is_baseline)
862
  judge_result["_a_is_baseline"] = a_is_baseline
863
+ _dt = time.time() - _t_judge
864
+ log.info(f"POST-L5: Judge done ({_dt:.1f}s)")
865
+ yield send("log", {"level": "INFO", "msg": f"Judge completed ({_dt:.1f}s)", "time": ts()})
866
  yield send("layer", {"id": "JUDGE", "status": "pass"})
867
  yield send("judge", judge_result)
868
  except Exception as e:
869
+ log.error(f"POST-L5: Judge FAILED after {time.time()-_t_judge:.1f}s: {e}")
870
+ yield send("log", {"level": "ERROR", "msg": f"Judge failed ({time.time()-_t_judge:.1f}s): {e}", "time": ts()})
871
  yield send("layer", {"id": "JUDGE", "status": "fail"})
872
 
873
  # ── Provenance ────────────────────────────────────────────────
 
891
 
892
  # ── Report generation ─────────────────────────────────────────
893
  report_id = None
894
+ log.info("POST-L5: starting Report")
895
+ _t_report = time.time()
896
  yield send("status", {"msg": "Generating report…"})
897
+ yield send("log", {"level": "INFO", "msg": "Starting report generation…", "time": ts()})
898
  try:
899
  from report import generate_report, save_temp
900
  report_md = await loop.run_in_executor(_executor, lambda: generate_report(
901
  client, MODEL_GENERATION, query, domain, weights,
902
  iterations, boundary, all_sources,
903
  baseline=baseline_answer))
904
+ _dt = time.time() - _t_report
905
+ log.info(f"POST-L5: Report done ({_dt:.1f}s)")
906
+ yield send("log", {"level": "INFO", "msg": f"Report generated ({_dt:.1f}s)", "time": ts()})
907
  report_id = secrets.token_hex(8)
908
  _reports[report_id] = {"md": report_md, "query": query, "domain": domain}
909
  yield send("report", {"report_id": report_id, "length": len(report_md)})
910
  except Exception as e:
911
+ log.error(f"POST-L5: Report FAILED after {time.time()-_t_report:.1f}s: {e}")
912
+ yield send("log", {"level": "ERROR", "msg": f"Report failed ({time.time()-_t_report:.1f}s): {e}", "time": ts()})
913
 
914
  # ── L6: S-genuine review gate β€” BLOCKS until decision ────────
915
  review_required = sims["high_stakes"]
 
1003
  # No event in `interval` seconds β€” send keepalive
1004
  yield ": heartbeat\n\n"
1005
 
1006
+ return StreamingResponse(
1007
+ heartbeat_wrap(event_stream()),
1008
+ media_type="text/event-stream",
1009
+ headers={
1010
+ "Cache-Control": "no-cache, no-transform",
1011
+ "X-Accel-Buffering": "no", # nginx: disable proxy buffering
1012
+ "Connection": "keep-alive",
1013
+ },
1014
+ )
1015
 
1016
 
1017
  # ── Entry point ──────────────────────────────────────────────────────────────
static/index.html CHANGED
@@ -14,7 +14,7 @@
14
 
15
  <nav>
16
  <div class="nav-inner">
17
- <div class="logo">CRCS<small>HYBRID ENGINE v0.21</small></div>
18
  <div class="nav-right">
19
  <button class="tab-btn active" data-tab="pipeline" onclick="switchTab('pipeline', this)">Pipeline</button>
20
  <button class="tab-btn" data-tab="kg" onclick="switchTab('kg', this)">KG Rules</button>
@@ -131,7 +131,7 @@
131
 
132
  </div>
133
 
134
- <footer>CRCS Hybrid Engine v0.21.0 &mdash; Constraint-Regulated Cognition &mdash; <span id="buildInfo" style="font-family:var(--mono);font-size:0.7rem;"></span></footer>
135
 
136
  <script src="/static/js/app.js"></script>
137
  </body>
 
14
 
15
  <nav>
16
  <div class="nav-inner">
17
+ <div class="logo">CRCS<small>HYBRID ENGINE v0.21.1</small></div>
18
  <div class="nav-right">
19
  <button class="tab-btn active" data-tab="pipeline" onclick="switchTab('pipeline', this)">Pipeline</button>
20
  <button class="tab-btn" data-tab="kg" onclick="switchTab('kg', this)">KG Rules</button>
 
131
 
132
  </div>
133
 
134
+ <footer>CRCS Hybrid Engine v0.21.1 &mdash; Constraint-Regulated Cognition &mdash; <span id="buildInfo" style="font-family:var(--mono);font-size:0.7rem;"></span></footer>
135
 
136
  <script src="/static/js/app.js"></script>
137
  </body>
static/js/app.js CHANGED
@@ -325,6 +325,8 @@ async function runPipelineSSE() {
325
  max_iter: 3,
326
  n_candidates: 3,
327
  }),
 
 
328
  });
329
 
330
  if (!resp.ok) {
@@ -850,9 +852,21 @@ async function runPipelineSSE() {
850
  }
851
  }
852
  } catch (e) {
 
 
 
 
 
 
 
 
 
 
 
853
  area.insertAdjacentHTML('beforeend',
854
- `<div class="error-note"><strong>Pipeline error:</strong> ${esc(e.message)}</div>`);
855
- dbg('ERROR', '', 'Pipeline error: ' + e.message);
 
856
  }
857
 
858
  state.running = false;
 
325
  max_iter: 3,
326
  n_candidates: 3,
327
  }),
328
+ // 10-minute timeout β€” pipeline can take several minutes for all layers
329
+ signal: AbortSignal.timeout(600000),
330
  });
331
 
332
  if (!resp.ok) {
 
852
  }
853
  }
854
  } catch (e) {
855
+ // Find which layer was last running to show context
856
+ const runningLayers = area.querySelectorAll('.layer-card');
857
+ let lastLayer = 'unknown';
858
+ runningLayers.forEach(el => {
859
+ const nameEl = el.querySelector('.layer-name');
860
+ if (nameEl) lastLayer = nameEl.textContent;
861
+ });
862
+ const elapsed = ((Date.now() - dbgRunStart) / 1000).toFixed(0);
863
+ const detail = e.name === 'TimeoutError' ? 'Browser timeout (10 min) β€” pipeline took too long'
864
+ : e.message === 'network error' ? 'Connection dropped β€” likely proxy timeout on HF Spaces'
865
+ : e.message;
866
  area.insertAdjacentHTML('beforeend',
867
+ `<div class="error-note"><strong>Pipeline error:</strong> ${esc(detail)}<br>` +
868
+ `<small style="color:var(--muted)">After ${elapsed}s Β· Last active: ${esc(lastLayer)}</small></div>`);
869
+ dbg('ERROR', '', `Pipeline error after ${elapsed}s at "${lastLayer}": ${e.message}`);
870
  }
871
 
872
  state.running = false;