feat(noema-agent): add calendar conflict-check tool - #1486
Conversation
Noema is the org's review bot in .github but needs a role suited to naruon's own workspace instead of a copy of that role. Its existing naruon agent (services/noema_agent.py) already covers mail/content-graph/ tasks/writeback; add scheduling-conflict judgment (PRD-02) by reusing the same deterministic, status-weighted policy the /api/calendar/conflicts endpoint applies, so the agent's judgment never diverges from that API. Naruon does not persist provider calendar events server-side, so the tool evaluates whatever commitments the caller already knows about rather than fetching a provider calendar itself; malformed existing-commitment rows are skipped instead of raised.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (12)
💤 Files with no reviewable changes (4)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe PR adds workspace-scoped email handling, attachment reparsing, persisted calendar conflict judgments and corrections, a fail-closed Noema conflict tool, and migration, worker, and CI workflow updates. ChangesWorkflow changes
Estimated code review effort: 5 (Critical) | ~120 minutes Merge Risk: 🟡 Moderate · up to This PR adds calendar conflict checking while also changing attachment reprocessing, workspace-scoped email/project-graph handling, fixture imports, and worker transactions. Open issues could mis-scope persisted data, suppress valid imports, bypass required configuration registration, or hold database snapshots during external calls, so merge should wait for fixes or explicit owner acceptance. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
PR governance metadata gate is not ready for
|
|
Status on this PR's two failing required checks — neither traces to this PR's diff:
Not fixing the central sidecar/free-pool gap from this PR — it's org-wide infra tracked in the central repo, not scoped to naruon. I've recorded this run's new evidence there. Keeping this PR watched; will re-check once required checks re-run. Generated by Claude Code |
|
One more failing check, distinct mechanism from the Generated by Claude Code |
|
Follow-up on the Generated by Claude Code |
…xtualwisdomlab-commercialization-afow1j
Devin review on #1486: existing[:_MAX_EXISTING_COMMITMENTS] silently dropped every commitment past the 500th instead of rejecting the evaluation, so a real conflict past that boundary could produce a false "available" decision. Return calendar_existing_batch_exceeded instead, matching the REST endpoint's own bound.
|
@coderabbitai review Generated by Claude Code |
|
|
Closes the missing "human correction" leg of the G-06 killer-workflow pipeline (thread/sender ontology -> temporal commitment/conflict -> human correction), whose temporal-conflict half PR #1486 already ships via check_calendar_conflict. evaluate_calendar_conflicts's decision was computed and handed back in-memory only, with nowhere for a human to record disagreement -- there is now a real judgment record and audit trail, mirroring the existing project_graph_corrections pattern. - New tables (Alembic 0018, structured ops): calendar_conflict_judgments (a persisted decision, status_code proposed/confirmed/overridden/ dismissed) and calendar_conflict_corrections (before/after JSON audit trail per correction, same shape as ProjectGraphCorrectionRecord). - POST /api/calendar/conflicts/judgments evaluates the same policy as /evaluate (now shared via _resolve_commitments) and persists the result, optionally tagged with source_thread_id/source_message_id. - GET /api/calendar/conflicts/judgments lists judgments scoped to the caller, optionally filtered by source_thread_id. - POST /api/calendar/conflicts/judgments/{judgment_uid}/corrections records a human override/confirm/dismiss with a full before/after snapshot; 404s (not silent no-op) outside the caller's own scope. - /evaluate's own stateless contract is unchanged. Verified: targeted tests/test_calendar_conflict_judgment_service.py + tests/test_calendar_conflict_judgment_api.py (8 passed); full backend suite python -m pytest -q (1821 passed, 32 skipped, no new skips); ruff check clean; alembic heads resolves to one head (0018_calendar_conflict_judgments); test_alembic_migrations.py + test_email_model_reconciliation.py + test_release_governance.py (58 passed). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr
…on API - Lock the target judgment row (SELECT ... FOR UPDATE) before snapshotting and mutating it in apply_correction, so concurrent corrections can no longer read the same prior state and race on the final decision/status. - When a correction changes decision_code, replace reason_code and recommended_action together (corrected_by_human_review + the rationale), so a later read can never pair a corrected decision with the previous decision's now-stale reason/instruction. The original values are never lost -- they are exactly what the correction's before_json captures. - Bound list_judgments to 200 rows so a long-lived account's history can't grow an unbounded query/response. - Deduplicate MAX_EXISTING_COMMITMENTS: it lived separately in api/calendar_conflicts.py and services/noema_agent.py with only a comment tying them together. Moved it to services/calendar_conflict_policy.py (already a shared import for both) so the two enforcement points cannot silently drift apart. - Fix the double import style of api.calendar_conflicts in tests/test_calendar_conflict_judgment_api.py (github-code-quality finding). Not fixed: a live-PostgreSQL concurrency test proving the row lock under real concurrent transactions -- this session has no PostgreSQL access, the same limitation test_project_graph_api.py's own analogous correction tests already carry (skip-when-unavailable). Left as a PR comment rather than a fabricated test. Verified: tests/test_calendar_conflict_judgment_service.py + tests/test_calendar_conflict_judgment_api.py + tests/test_noema_agent.py + tests/test_calendar_conflict_api.py + tests/test_calendar_conflict_policy.py (65 passed); full backend suite python -m pytest -q (1825 passed, 32 skipped, no new skips); ruff check clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr
…ings
- [security, highest severity] Scope calendar_conflict_judgments/
corrections by workspace_id, not just user_id+organization_id.
AuthContext.workspace_id is an independent session-token claim (only
the test stub derives it from user_id/org for convenience), so the
same user_id+organization_id can legitimately operate under different
workspace_id values -- the prior scoping let a caller list/correct
another workspace's judgments. Follows the same workspace_id scoping
already established by the project_graph module. Alembic 0018 is
amended in place (not a new migration) since it has never been
applied to any database -- this whole feature is still an open,
unmerged PR.
- Add GET /api/calendar/conflicts/judgments/{judgment_uid}: a judgment
older than list_judgments' 200-row bound is otherwise unreachable by
any caller who already knows its uid. Full pagination remains future
work; single-item retrieval is the small, proportionate fix for "how
do I get back to a specific judgment."
- Stop storing a correction's rationale as recommended_action. Added
calendar_conflict_policy.default_recommended_action() as the single
source of truth for decision_code -> recommended_action (also now
used by evaluate_calendar_conflicts itself, removing the previous
inline-string duplication); apply_correction restates recommended_action
from it instead of the rationale, which explains why a human overrode
a decision but is not itself forward-looking scheduling guidance.
- Add validate_correction_coherence(): a correction's status_code and
decision_code must agree (override requires a replacement decision;
confirm/dismiss must not change one). Enforced both in the API request
model (fast, specific 422) and in apply_correction (non-HTTP callers).
- Deduplicate the ICS parser's hardcoded 500-commitment bound into the
same shared MAX_EXISTING_COMMITMENTS constant already used by the API
and the Noema tool.
- Noema's check_calendar_conflict now reports skipped_existing_count so
a caller can tell a clean "available" from one computed after silently
dropping malformed evidence.
Verified: full backend suite python -m pytest -q (1835 passed, 32
skipped -- one unrelated process-group-timing test flaked only under
full-suite concurrency and passed in isolation, confirmed unrelated to
this diff); ruff check clean; alembic heads resolves to one head.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr
… doc - apply_correction only replaces reason_code/recommended_action when decision_code actually differs from the judgment's current value. An override that repeats the current decision is otherwise a no-op on the decision itself and must not erase an original, still-accurate reason/action for no real change. - docs/doctoring/status-weighted-calendar-conflicts.md still described the calendar-conflict feature as introducing no database objects. Updated its shipped boundary, rollback sequence (downgrading 0018 is destructive once real corrections exist), and verification-evidence section to cover the judgment/correction persistence slice; the stateless /evaluate endpoint's own boundary is unchanged. Verified: new regression test for the no-op-override case; full backend suite python -m pytest -q (1836 passed, 32 skipped); ruff check clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr
Devin review on #1486: my own previous doc update said validate_correction_coherence() rejects a no-op override (one that repeats the judgment's current decision), but the actual contract is the opposite -- that case passes coherence validation and is accepted; apply_correction just skips the reason/action replacement for it. Reworded to state both facts without conflating them.
…_uid + reparse-intent (G-15)
Sniffs magic bytes (PDF/PNG/JPEG/GIF/ZIP) against the declared/resolved
content_type in services/attachment_parser.py; a mismatch is quarantined
(content_type_mismatch_quarantined) instead of silently parsed or
misclassified, retaining the raw bytes for later re-evaluation. Adds
Attachment.attachment_uid (Alembic 0019, backfilled) so a quarantined
attachment is individually addressable, and POST
/api/data/attachments/{attachment_uid}/reparse-intent to record a
re-evaluation request, matching the existing hwp-conversion-intent /
pdf-dom-recognition-intent pattern (intent recorded, no synchronous
re-parse). Design rationale in docs/adr/0005-attachment-content-type-quarantine.md.
Verification: full backend suite 1842 passed/33 skipped (8 new tests),
ruff clean, alembic heads resolves to a single 0019_attachment_uid head.
…able oversized reparse, error_code convention Addresses three verified Devin/CodeRabbit findings on naruon#1486: - DOCX/XLSX/PPTX (and other ZIP-based container formats: ODF, EPUB, JAR) were quarantined as content-type mismatches purely because they sniff as application/zip, which is what those formats correctly are. _is_genuine_content_type_mismatch now excludes a ZIP sniff whose declared type is itself a known ZIP-container family (matched by MIME substring); a ZIP sniffed under any other declared type is still quarantined. - An oversized mismatched attachment retained no bytes but still got content_type_mismatch_quarantined, so the reparse-intent API would accept a request for a row with nothing left to act on. It now gets the existing parse_size_limit_exceeded status instead, matching every other oversized attachment already in this parser. - apply_correction's status_code/decision_code validation raised text-only ValueError; replaced with CalendarConflictUnsupportedValueError (mirrors CalendarPolicyValidationError's error_code attribute pattern) and mapped by type at the API layer, matching this repo's error_code convention. Currently unreachable via the HTTP route (Literal-typed request fields already reject bad values), kept as defense-in-depth for non-HTTP callers. Documented, not silently patched: _get_scoped_attachment's lack of workspace_id scoping is real but pre-existing -- Email itself has never had a workspace_id column, unlike every workspace-scoped entity added since. Properly closing it needs an Email migration plus updating every existing email/attachment query, well outside this PR; recorded in docs/adr/0005-attachment-content-type-quarantine.md's Consequences as a tracked follow-up. Verification: full backend suite 1845 passed/33 skipped, ruff clean.
|
The required This is not this PR's failure: it's the same "healthz passes then the real completion request hangs with 0 bytes" gateway-hang signature already tracked as a distinct, open issue in Generated by Claude Code |
…ding (G-15) ADR-0005 deliberately deferred the worker that actually re-evaluates a quarantined attachment after reparse-intent, mirroring the NewsDOM PDF worker's own separate-follow-up precedent. This ships that worker. AttachmentReparseWorker (services/attachment_reparse_worker.py) mirrors NewsdomRecognitionWorker's shape (jittered loop, PostgreSQL advisory-lock lease, starvation-free cursor, per-item error isolation), wired into main.py's lifespan alongside it. Every sweep re-decodes a reparse_pending attachment's retained bytes and replays parse_email_attachment against them and the attachment's original declared content_type -- deliberately not the sniffed type, so the worker carries no bespoke "which type do I trust" logic and automatically benefits from any future classification fix (as the already-shipped OOXML carve-out would have, had it existed first). A row that's no longer a genuine mismatch escapes quarantine into its ordinary classification; a genuine one lands back in quarantine. Added decode_quarantined_attachment_payload (a non-PDF-specific sibling of decode_deferred_attachment_payload) since a quarantined attachment's sniffed type can be any of the magic-byte families this parser recognizes, not just PDF. A retained payload that isn't valid base64 moves to a new terminal reparse_payload_invalid status instead of being swept forever. Updated ADR-0005 (Consequences + a Revisions entry) and its README index row, both of which said "reparse_pending has no consumer worker yet." Verification: 19 new tests (worker: process-level outcomes, sweep pagination/wrap/isolation, lease honor, start/stop, loop behavior; parser: the new decoder's round-trip and rejection paths). Full suite 1864 passed/33 skipped (was 1845), ruff clean.
|
Fixed the "older requests disappear behind cursors" finding ( Verified this is a live, real path, not just theoretical: Fix: every Fixing this surfaced a latent bug the change would otherwise have triggered: the query builders restricted results to only Verified test-first in both workers: reproduced each row staying invisible across many sweeps against the pre-fix code, then confirmed it gets rediscovered at exactly the 20th sweep after the fix.
Generated by Claude Code |
|
Responding to this round's three Devin Review notes on head "Worker retry paths remain reachable" ( "Baseline migration depends on mutable code" ( Generated by Claude Code |
…-commercialization-afow1j' into claude/attachment-reparse-content-graph-index # Conflicts: # CHANGELOG.md
Merging origin/claude/noema-contextualwisdomlab-commercialization-afow1j (the cursor/retry-set starvation fix, which added _LiveReparsePendingSession in tests/test_attachment_reparse_worker.py) into this PR's branch (which added the session.refresh(attachment, attribute_names=[...]) call in _sweep_attachments, needed to eager-load relationships before apply_reparsed_result appends content-graph rows through them) surfaced an integration gap neither branch could have caught alone: the merged production code now calls session.refresh() on every sweep, but _LiveReparsePendingSession (used by two multi-sweep scheduling tests) never implemented it, since it predates that call. Add a no-op refresh(), matching the sibling _SequenceSession fake's pattern. Confirmed via RED (test_sweep_does_not_starve_rows_behind_many_failing_rows and test_sweep_rediscovers_a_row_reverted_to_pending_behind_the_cursor both failed with AttributeError before this fix) -> GREEN (full backend suite: 1920 passed, 43 skipped; ruff clean). One unrelated timing-sensitive test (test_main_kills_original_process_group_on_timeout) failed once under full-suite load and passed in isolation and on a second full-suite run -- a pre-existing flake, not caused by this merge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4GKADWJyd8NToEAK5SH6Q
|
Status update on the governance-gate blocker:
No code changes needed on this PR beyond what's already pushed ( Generated by Claude Code |
|
Update: the organization-wide Actions queue saturation has drained — nearly every check on this PR just went green (CodeQL, Bandit, Semgrep, scorecard, osv-scan, trivy-fs, dependency-review, frontend/backend/security, all image validations). Two failures remain, both diagnosed as infra-caused, not defects in this PR's own diff:
Will keep watching for both to complete. Generated by Claude Code |
…graph (#1501) Indexes a successfully-reparsed attachment's recognized content into the content graph, matching the existing first-import indexing path. Devin Review clean on exact head d7e5d2d; all 4 required checks (Bandit, Application CI, Dependency Review, Docker build) green after the org-wide Actions runner-starvation backlog cleared. Full backend suite 1920 passed/43 skipped, ruff clean.
|
Fresh dependency-gate check: the earlier correction away from direct tenant-provider routing is directionally right, but this lane still cannot be treated as production-ready under the current immutable-owner rule. Current exact head is Current acceptance is Naruon #1540 / contextual-orchestrator #1023: preserve Naruon's valid calendar-conflict tool/domain work and fail-closed behavior, but do not make Naruon a second owner of CO API/model/pool semantics. After CO publishes an immutable API/client/schema/runtime identity with exact version/digest/source provenance and auth/failure contracts, this lane should consume that released surface through an ACL, remove/reconcile locally copied gateway semantics, and rerun its exact-head Python 3.14/security/coverage/review evidence. If the dependency is absent or incompatible, Noema remains This is an owner-path note only; no write was made to the externally owned branch. |
|
Acknowledged and verified against #1540/#1023 (both real, both tracking the same underlying "immature core" concern applied consistently across Checked this PR's No code change to this PR right now: the remediation path (consume the released API/client/schema via an ACL once CO publishes it, rerun exact-head evidence) is correctly gated on CO's own release, tracked in #1023, not something this PR can do today. Leaving #1540/#1023 as the durable tracking record rather than duplicating it here. _Generated by Claude Code Generated by Claude Code |
…ref fix, new security gap G-24) - Qualify two newly-added bare `naruon#1486` references to `ContextualWisdomLab/naruon#1486` per CWL-MASTER-CONTEXT.md's cross-repo reference convention (same-repo `.github#1659` already complied and was left as-is). - Record G-24: the owner's verbatim Keyverse Direct Grant/ROPC mandate hands raw credentials to product-owned login forms, a real, previously-untracked security trade-off. Not fixed by rewording the owner's explicit directive text -- recorded as a gap directing a future Keyverse-integration ADR to make an explicit decision. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr
|
Lifecycle repair on exact head The calendar-conflict domain slice itself is not being rejected. The blocker is the LLM/runtime integration mixed into this 85-file branch:
Repair direction: keep Naruon-owned calendar policy/tools, authorization, workspace context, and UI/domain truth here; move reusable Noema runtime/client capability to |
…tate DDL Semgrep OSS's sqlalchemy-execute-raw-query and formatted-sql-query rules flag both op.execute() calls in this migration as raw-SQL injection risk. Both are false positives: _UPGRADE_SQL/_DOWNGRADE_SQL interpolate only the fixed module-level literal _IS_READ_PROVENANCE_MARKER (already documented in the module docstring and covered by the existing `# nosec B608` comments) -- never external input or an identifier. Semgrep's pattern only recognizes "op.execute(f-string)" and can't see the interpolated value is a constant. No behavior change. Unblocks naruon#1486 (4 open review threads and 2 required check failures were exactly these 4 Semgrep findings on the same 2 lines). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Repair delivered; protected validation remains pendingThe prior runtime-reproduction handoff below is historical. The repair is now normally pushed to this existing owner PR at The exact-head doctoring record records the RED failures, choices, root repairs, reproduction command, and limitations. One physical connection now survives work commits/rollbacks; unlock must be confirmed, interrupted ownership invalidates the connection, and shared escalation conflict handling refreshes expired source objects without redundantly expunging failed inserts. Configuration is re-read at workspace boundaries. The unchanged exact head passed 165 tests (0 failures/errors/skips, 2.49s), including 12 actual migrated PostgreSQL cases. The two changed runtime modules have 259/259 statements, 82/82 branches, zero exclusions, 24/24 definition docstrings. JUnit SHA-256: The intermediate test-import error at This PR remains Draft, open, unmerged, and unreleased. Current-head backend checks and Noema admission were queued at the recorded snapshot. Neither this comment nor local tests replace checks, independent exact-head review, or protected merge. #1317's separate import lease repair is not claimed complete. The public-mail replay does not establish realistic inbox accuracy, live authentication, or latency acceptance. Original handoff at b32954d — preserved historical evidenceRoot-repair handoff at _ sync lease flow, lines134–146 acquires and releases a session advisory lock through an ordinary engine-bound session. The called escalation service commits bulk/fallback work and rolls back on integrity conflict before scheduler unlock. Keeping that Python session does not preserve the physical backend across these boundaries. #1469 reproduced lease leakage after both commit and rollback with an unrelated pooled reader and an independent PostgreSQL replica. For this owner, reproduce with actual migrated escalation writes, retain one physical lease holder across the entire owner/workspace sweep, require confirmed unlock, invalidate uncertainty before session-close rollback, and abort connection loss instead of continuing unleased. Preserve this PR's workspace iteration and other valid deltas. Cover same-pool contention, independent-replica reacquisition, supported pool capacity, actual task cancellation and cleanup ordering, connection loss, and owner/workspace recovery after healthy rollback. Existing tests/test_reply_sla_scheduler.py fakes and the manual API smoke in tests/test_tasks_api.py do not prove scheduler contention behavior. Worker-specific RED/GREEN and alternatives are investigation evidence, not copied source or scheduler GREEN. No scheduler source was changed by #1469; this is not approval, protected merge, or an exactly-once external-execution claim. |
Retain all existing PR1486 delta. Bind the sweep session to one physical connection, invalidate before cleanup, recover expired rows and savepoint conflicts, and revalidate mailbox configuration between workspaces. Preserve competing task identity and source workspace scope. Actual migrated PostgreSQL RED/GREEN covers pool lending, rollback, disconnect, cancellation, concurrent task creation and configuration deletion. Reviewed candidate:165 tests pass; two runtime modules259statements/82branches100%;24/24 docstrings. Exact-head verification follows. Reuse dev-only httpx2 pin and remove obsolete warning ignore; no source/workflow deletion or release claim. Co-Authored-By: OpenAI Codex <noreply@openai.com>
Correct the test-only mechanical rename of db.models and record the failed intermediate collection. The 12 focused edge tests and Ruff pass again; a full exact-head migrated PostgreSQL run is still required before push. No history rewrite. Co-Authored-By: OpenAI Codex <noreply@openai.com>
| await release_lease(replica_session) | ||
| cleanup_allowed.set() | ||
| with pytest.raises(asyncio.CancelledError): | ||
| await sweep_task |
Current exact-head scheduler repair
Head
1709ebb8d79f55c688a141aa932fa00468bf836d, tree5a39d4eef5a936969b14f649af21638df24a90f1, direct base042b0c70531b229af3acbd0421a2f23098d848b3. Two ordinary commits preserve the fullb32954dbf6066bc0d953887e8ca06820588f2c5fproposal. No force push, predecessor closure, source-file/workflow removal, model-routing change or deployment.Actual PostgreSQL RED→GREEN covers a held connection lent to a pool reader after task commit, healthy-owner rollback recovery, aborted disconnected sweeps, uncertain acquisition/task cancellation, one-slot cleanup, real competing task inserts at bulk/savepoint boundaries, and mailbox deletion after the first workspace. The scheduler binds its existing session to one physical connection, confirms unlock, invalidates before session cleanup, and forces configuration revalidation. The shared task service refreshes expired mail after conflict rollback and no longer expunges already-detached savepoint inserts. Existing source-task identity and workspace boundaries remain intact. Independent read-only review findings were reproduced and repaired.
Final committed-head verification: 165 passed, 0 failed/errors/skips, 2.49 s, after
uv sync --locked, fresh migration and repeat migration to0022_noema_orchestrator_gateway; clean tree and head identity checked before/after. Scheduler 106/106 statements, 28/28 branches; escalation 153/153 statements, 54/54 branches; zero exclusions, 24/24 function/class/method docstrings. Twelve real-DB cases and twelve scripted unit-only edge cases are explicitly separated. Ruff passes and final runner output contains no Timeout/Fatal/Warn/Denied matches.cd backend uv sync --locked uv run --locked python scripts/migrate_db.py uv run --locked python scripts/migrate_db.py uv run --locked coverage run --branch --source=services.reply_sla_scheduler,services.reply_sla_escalation_service -m pytest -q -W error -ra --tb=short tests/test_reply_sla_scheduler.py tests/test_reply_sla_scheduler_postgres.py tests/test_reply_sla_escalation_edges.py tests/test_tasks_api.py tests/test_reply_tracking.py tests/test_reply_tracking_service.py tests/test_db_session.py tests/test_alembic_migrations.py tests/test_bootstrap_db.py uv run --locked coverage report -mUse only an isolated PostgreSQL and generated test credentials. JUnit SHA-256
7ab943b0ceffcb26a72c802272cde48f410282c4cfe6e76bee5905725333a69f; coverage JSON SHA-256207cd65f29b945c2f9f3d40f75bcb081f29da0c4e1744648edd7ebdea24a4c5d. Runtime source SHA-256: schedulerf2968660d77a69226e8acc95bdb92b3a4ed8115f564c8eed5dcdb9375d1930f7; service7d8bbd424d79dae2cb84df4dd403415ecaa5dee69e973d5236412eaa3922f927.Doctoring, Proposed ADR supplement, sequence and primary references preserve RED receipts and the failed intermediate test-only import rename; local
7ce6592is not a GREEN receipt. The dev-only existing Naruonhttpx2==2.5.0pin removes Starlette's deprecated test-client fallback and obsolete ignore; production dependencies unchanged. ADR-0005 and index are corrected to Proposed, because the decision file is absent from protected develop. The existing AGENTS PR #1566 receives the reusable procedures; the Gap ledger stays with #1557.Still Draft, unmerged, unreleased. Fresh enumeration found queued checks and skipped non-applicable jobs, no current-head reviews. These scoped results do not certify all other changes in this broad PR, repository-wide coverage, representative inbox accuracy, live signed HTTP/browser behavior, transaction-pooling proxies, exactly-once execution, realistic p95/capacity or default-branch security-alert closure. Existing API synthetic/metadata smoke is not migration or live-auth proof. Required checks/review, owner foundation/release adoption and protected merge remain open. Test-owned database/network cleanup was verified without touching other projects.
Preserved original proposal and historical evidence
Description
Noema is used as the org's review agent in
ContextualWisdomLab/.github, but naruon needs its own, broader role for Noema rather than a copy of that reviewer role — it should be a genuinely general-purpose workspace assistant. Naruon already has a distinctnoema-general-agent(backend/services/noema_agent.py) that reasons over mail, the content graph, and tasks. This PR widens that agent's own domain coverage.It adds a
check_calendar_conflicttool so Noema can judge whether a proposed meeting time double-books an existing commitment (PRD-02 indocs/product-technical-gap-baseline.md— "일정 이동과 RSVP/commitment 충돌을 놓치지 않는다"). Naruon already has a stateless, deterministic, well-tested conflict policy behindPOST /api/calendar/conflicts/evaluate(services/calendar_conflict_policy.py::evaluate_calendar_conflicts— status-weighted confirmed > tentative > desired, RFC 5545STATUS:CANCELLEDhandling). This PR reuses that exact function as a Noema tool instead of inventing a second conflict policy, so Noema's judgment and the customer-facing API can never diverge.Naruon does not persist provider calendar events server-side (customer systems stay the source of truth), so the tool never fetches a provider calendar itself — it evaluates whatever commitments the caller (the LLM, after reading mail/task evidence earlier in the same run) already supplies. A malformed
existingrow is skipped rather than raised, so one bad entry can't block judgment on the rest; the proposed commitment itself is validated strictly and reported as a typed error (reusingCalendarPolicyValidationError's existing error codes).Also documents, in
registered_agents.json, that naruon's Noema (noema-general-agent) is a distinct deployment from the central.githubCI review-bot Noema.2026-09-02 architecture correction (owner-reported): this PR originally routed the agent's LLM calls through the tenant's own directly-configured LLM provider (
resolve_runtime_llm_provider→AsyncOpenAIwith the tenant'sbase_url/api_key). The owner correctly flagged that this makes naruon a second LLM-provider-routing authority, which conflicts with naruon's ownership boundary: production LLM routing belongs exclusively toContextualWisdomLab/contextual-orchestrator; naruon owns the Noema tools/authorization/context, not a second routing authority.Fixed test-first: a RED test proved
run_noema_agentreachedresolve_runtime_llm_provider/directAsyncOpenAIconstruction against the pre-fix code. The fix addsbackend/services/orchestrator_gateway.py(resolve_orchestrator_gateway/OrchestratorGateway), which resolves a new, tenant-scoped, SSRF-validatedtenant_configs.noema_orchestrator_base_url/noema_orchestrator_tokenpair (Alembic0022_noema_orchestrator_gateway, mirroring the existing in-productionbatch_orchestrator_base_url/batch_orchestrator_tokenpattern from migration0012) and always sends a fixedcontextual-orchestratormodel alias — naruon never picks or fails over across upstream models itself. When the gateway is unconfigured or invalid,noema_agent.pynow returns a structuredstatus="unavailable"/error_code="orchestrator_gateway_unavailable"result rather than ever falling back to a direct provider. This tenant-scoped credential stays distinct from.github's CI review credential (naruon workspace data never crosses that path), which is a security-scoping choice for this module, not a claim about the two deployments' overall relationship — see the next correction.2026-09-02 second correction (owner-reported, superseding the "two separate agents" framing above and in the pre-existing PR text): the "naruon's Noema and
.github's review-bot Noema are two separate agents that share only a name" framing (both the sentence in this description and this PR'sdocs/adr/0006-noema-bounded-context-separation.md-adjacent reasoning) is wrong. Perdocs/CWL-MASTER-CONTEXT.md(ContextualWisdomLab/.github), Noema is one shared agent runtime (Pydantic-AI/Codex-Python) consumed by naruon, the.githubCI review agent, and wardnet's AI SOC quarantine sandbox — this was the intended design from the start, confirmed directly by the owner, not a naming coincidence.docs/adr/0006-noema-bounded-context-separation.md(naruon#1527) has been updated in place to mark its "therefore keep them permanently separate" conclusion superseded-on-arrival; the code-level investigation of what each deployment currently does remains accurate, and the actual shared-runtime design is a separate, not-yet-settled follow-up. The calendar-conflict tool work in this PR, and the LLM-routing fix above, are unaffected by this correction.Type of change
Checklist
backend/tests/test_noema_agent.py: 22 passed, 0 skipped, including the real pydantic-ai/AsyncOpenAIbuild path exercised locally withpydantic-ai-slim[openai]==2.9.0pinned inbackend/requirements-agent.txt)PYTHONPATH=. python -m pytest backend/tests/test_noema_agent.py -q→ 22 passed; full suitePYTHONWARNINGS=error DISABLE_BACKGROUND_WORKERS=1 python -m pytest -q→ 1908 passed, 39 skipped;ruff checkclean on changed filesFixes: none (product-gap increment, not a bug fix); tracks
docs/product-technical-gap-baseline.mdPRD-02 / G-06. Architecture correction responds to the owner's finding on this PR (naruon must not act as a second LLM-provider-routing authority); the second correction responds to the owner's follow-up on the "separate agents" framing (naruon#1527 /docs/CWL-MASTER-CONTEXT.md).🤖 Generated with Claude Code
https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr
Summary by CodeRabbit