Skip to content

feat(otel): LangGraph + OpenLLMetry recipe, CI-tested against a live ClawMetry (a run is no longer split in two) - #5972

Merged
vivekchand merged 8 commits into
mainfrom
feat/otel-langgraph-recipe-5939
Sep 14, 2026
Merged

vivekchand merged 8 commits into
mainfrom
feat/otel-langgraph-recipe-5939

Conversation

@vivekchand

Copy link
Copy Markdown
Owner

Refs #5939

Requirement: https://factory.8090.ai/project/b415065f-ab2f-4f53-8864-0c009fd098cb/requirements/0130f6a6-3f5b-448f-bd59-d8820a4d5c88 (REQ-OBS-OTR-001, child of Runtime and Session Observability; written before the code, blueprint filled in)

Branched off main. Independent of #5953 (spans reach Guard) and #5963 (durable ack): either merge order works. The Guard line of the issue stays with #5953.

Scope: the pilot-first increment

The issue's refinement asks for one real framework and exporter version first, CI-tested end to end, with other frameworks added only as separately validated increments. This PR is that first increment: LangGraph + OpenLLMetry. Azure AI Foundry and AWS AgentCore are not included and are listed below.

What running the real thing found

A one-tool LangGraph agent (langgraph 1.2.11, opentelemetry-instrumentation-langchain 0.62.3, opentelemetry-sdk 1.44.0) exported to a scratch dashboard came back as two sessions for one run:

session tokens tool call
t-1 (the thread) 0 none
proto:trace:<trace_id> 307 lookup_invoice

OpenLLMetry sends the thread as gen_ai.conversation.id on the top invoke_agent span only. Every span beneath it (both model calls and the tool call) carries it as traceloop.association.properties.thread_id. _otel_to_row read only the first, so the children fell through to the per-trace fallback.

Fix (one pick list in dashboard.py::_otel_to_row): read traceloop.association.properties.thread_id right after gen_ai.conversation.id and before session.id, recorded as sent. Precedence: sent conversation id, thread, session.id, then <app>:trace:<trace_id>. It is not prefixed, because the top span already sends the same value bare; a prefix would recreate the split.

What ships

Part File
Recipe: one tool, deterministic stub model (no key), --thread, --stream examples/otel/langgraph/
Live verifier (stdlib), reads back only through /api/local/traces + /api/local/spans scripts/verify_otel_recipe_langgraph.py
CI job otel-recipe-langgraph: recipe in its own venv from a hash-pinned lockfile, dashboard on 0.0.0.0, agent exports through the runner's non-loopback IP .github/workflows/ci.yml, .github/requirements/otel-recipe-langgraph.txt
Hermetic guard: exports captured from the recipe itself (not hand-written) replayed through the receiver; pins recipe, lockfile, CI job and page to the same versions tests/test_otel_recipe_langgraph.py, tests/fixtures/otel/
Page: versions, encoding, semconv revision, span tree, identity and precedence, missing-id fallback and cardinality, per-tab behaviour, Bearer auth for a remote node, observe-only limits docs/OTEL_RECIPE_LANGGRAPH.md (linked from OPENTELEMETRY.md, BRING_YOUR_OWN_AGENT.md)

The verifier checks, each a hard failure with a sentence:

  • an export through a non-loopback address with no token gets 401; the recipe exporting with OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer … is accepted;
  • two runs on one thread (invoke() then stream()) are one session holding both traces;
  • each trace carries exactly the tokens the model reported (280 in / 27 out);
  • every span of each run is in that session; the lookup_invoice span descends from invoke_agent in the same trace; both model calls carry the model and a derived cost; all spans are attributed to invoice_agent;
  • a run with no thread is its own session invoice_agent:trace:<trace_id>.

Verified (locally, Python 3.11, scratch HOME and store, random port)

  • Red on main (dashboard.py swapped for origin/main's copy): 4 hermetic tests fail (thread_run_is_one_session_with_every_span, second_run_on_the_thread_joins_the_same_session, streamed_run_matches_invoked_run, thread_wins_over_resource_session_id), and the live verifier fails with trace …: tokens 0/0, the model reported 280/27.
  • Green with the fix: 11/11 in the new file; the live verifier prints PASS including the 401/Bearer step through the machine's LAN address.
  • Neighbouring OTLP suites unchanged: 87 passed across test_otel_recipe_langgraph, test_otlp_session_materialization, test_otlp_sessionless_spans, test_spans_otlp_edge_cases, test_otlp_runtime_profiles, test_otlp_traces_cost, test_otlp_json_no_protobuf.
  • check_ac_coverage --check (8 criteria mirrored, 117/187), check_ci_test_coverage --check (baseline tightened 920 to 919), gen_module_map --check, check_py39_annotations, ruff on the new files.
  • No new HTTP route, so no cloud_route_policy entry is needed.

Found while verifying, recorded on the page, not fixed here

  • Guard lists a span-only session but labels it openclaw, offers Pause/Stop, and suggests openclaw attach. None of that can reach a remote agent. The page says so and does not claim control. It belongs with the spans and Guard work (OTLP traces: spans bypass Guard detectors and redaction #5938).
  • Usage: on a daemon-free dashboard the daily totals did not include span-only tokens. The page marks Usage "not verified".
  • An unrecognised model alias still gets a fallback-rate cost, so the page tells the reader to check the model name.

Remaining for #5939 (not in this PR)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Visual diff

Comparing 887537a7c45e (head) against the PR base branch.

42 of 70 comparison(s) flagged (>1% pixel diff).

View Before After Diff
desktop overview before after diff · 0.00%
desktop flow before after diff · 0.08%
desktop brain ⚠️ before after diff · 7.39%
desktop usage ⚠️ before after diff · 100.00%
desktop crons before after diff · 0.00%
desktop memory before after diff · 0.02%
desktop security ⚠️ before after diff · 2.83%
desktop subagents ⚠️ before after diff · 100.00%
desktop transcripts before after diff · 0.01%
desktop logs ⚠️ before after diff · 100.00%
desktop skills before after diff · 0.01%
desktop models before after diff · 0.00%
desktop approvals ⚠️ before after diff · 100.00%
desktop alerts ⚠️ before after diff · 100.00%
desktop notifications before after diff · 0.36%
desktop limits before after diff · 0.00%
desktop history before after diff · 0.01%
desktop channels ⚠️ before after diff · 100.00%
desktop harness ⚠️ before after diff · 100.00%
desktop inventory ⚠️ before after diff · 100.00%
desktop nemoclaw ⚠️ before after diff · 100.00%
desktop guard ⚠️ before after diff · 2.74%
desktop signals ⚠️ before after diff · 3.10%
desktop policy ⚠️ before after diff · 100.00%
desktop selfevolve ⚠️ before after diff · 2.05%
desktop swimlane ⚠️ before after diff · 100.00%
desktop tool-catalog before after diff · 0.36%
desktop tracing ⚠️ before after diff · 2.59%
desktop turn-anatomy before after diff · 0.38%
desktop version-impact ⚠️ before after diff · 100.00%
desktop context-economics before after diff · 0.36%
desktop agents ⚠️ before after diff · 100.00%
desktop evals before after diff · 0.00%
desktop bench ⚠️ before after diff · 2.92%
desktop trail before after diff · 0.37%
mobile overview before after diff · 0.38%
mobile flow before after diff · 0.01%
mobile brain before after diff · 0.00%
mobile usage before after diff · 0.00%
mobile crons before after diff · 0.02%
mobile memory ⚠️ before after diff · 100.00%
mobile security ⚠️ before after diff · 3.94%
mobile subagents ⚠️ before after diff · 100.00%
mobile transcripts ⚠️ before after diff · 100.00%
mobile logs ⚠️ before after diff · 5.00%
mobile skills ⚠️ before after diff · 100.00%
mobile models ⚠️ before after diff · 100.00%
mobile approvals before after diff · 0.01%
mobile alerts before after diff · 0.00%
mobile notifications ⚠️ before after diff · 100.00%
mobile limits ⚠️ before after diff · 100.00%
mobile history before after diff · 0.99%
mobile channels ⚠️ before after diff · 2.52%
mobile harness ⚠️ before after diff · 100.00%
mobile inventory ⚠️ before after diff · 1.61%
mobile nemoclaw ⚠️ before after diff · 100.00%
mobile guard before after diff · 0.49%
mobile signals ⚠️ before after diff · 1.11%
mobile policy ⚠️ before after diff · 100.00%
mobile selfevolve ⚠️ before after diff · 3.65%
mobile swimlane ⚠️ before after diff · 1.72%
mobile tool-catalog ⚠️ before after diff · 1.61%
mobile tracing before after diff · 0.01%
mobile turn-anatomy ⚠️ before after diff · 1.02%
mobile version-impact ⚠️ before after diff · 100.00%
mobile context-economics before after diff · 0.00%
mobile agents ⚠️ before after diff · 1.61%
mobile evals before after diff · 0.01%
mobile bench ⚠️ before after diff · 4.38%
mobile trail before after diff · 0.01%

Folder: 887537a7c45e. Full PNGs also attached as a workflow artefact.

Generated by visual-diff bot. Pixel diffs >1% flagged; eyeball the table before merging. This check is non-blocking — fail = bot bug, not a code problem.

@vivekchand

Copy link
Copy Markdown
Owner Author

The first run of the new OTel recipe (LangGraph + OpenLLMetry) job was red at the boot step, before the recipe ran. The hermetic guard in the same job passed (11 tests).

Cause: readiness was probed on /api/local/health. On a fresh store path the DuckDB file does not exist until the receiver's first write creates it, so the read-only local API answers 503 {"error": "IO Error: Cannot open database ... in read-only mode: database does not exist"} and the loop never passed. The dashboard itself was up. It was still running at cleanup, and /api/health answered 200 after 3 to 4s.

Reproduced locally in a venv installed from .github/requirements/ci-tests.txt + ci-otel-extra.txt with --require-hashes: /api/health 200, /api/local/health 503 with that exact message.

Fix (f4c7641): readiness now probes /api/health, like every other job that boots dashboard.py, and the dashboard runs with python3 -u so a boot failure prints its log. Nothing in the recipe, the verifier or the receiver changed.

Rehearsed the job's steps locally before pushing: CI-pinned dashboard on 0.0.0.0, verifier with --remote-url through the LAN address. Result PASS: 401 without a token, accepted with a Bearer token, one session for the invoke and stream runs on a thread (614 tokens), the tool span under invoke_agent, and a per-trace session for the no-thread run.

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 14, 2026
@vivekchand

Copy link
Copy Markdown
Owner Author

Coordinator review

Verdict: fix first, one blocking item. Everything else checks out. Reviewed at f4c764102b, which is on current main (merge base = main HEAD 658342f).

Blocking

1. AC-OBS-OTR-001.4 says the opposite of what the PR ships.

Factory REQ-OBS-OTR-001 and its copy in docs/acceptance_criteria.json both read:

An explicitly sent conversation or session identifier shall take precedence over a thread identifier.

The PR does the reverse in three places:

  • dashboard.py::_otel_to_row: the pick order is gen_ai.conversation.id, then traceloop.association.properties.thread_id, then session.id.
  • The page's precedence list: thread (2) is above session.id (3).
  • The test cited for .4: test_thread_wins_over_resource_session_id asserts that the thread beats session.id.

This is not only about the resource session.id the test covers. _pick checks span attributes before resource attributes, key by key. So a session.id set on a span by the application also loses to the thread now.

The product record and its guard disagree, and Drift Bot did not catch it. Pick one:

  • (a) Keep the current behaviour. Put a tracked edit on AC .4 in Factory and mirror it in acceptance_criteria.json. Suggested wording: "a sent conversation identifier takes precedence over a thread identifier; a thread identifier takes precedence over session.id on the span or resource; each recorded as sent". Also fix the code comment, which says only the resource-level session.id is outranked.
  • (b) Keep the AC as written. Then a span-level session.id has to win over the thread, which needs a code change and a test. The resource-level question still needs an answer either way.

Verified (not taken from the builder report)

Non-blocking (worth doing in this PR or a follow-up)

  • Ingest contract. clawmetry/ingest_contract.py generates docs/INGEST.md, whose gen_ai.conversation.id alias row still lists only session.id, openclaw.session_id and session_id. The receiver now reads traceloop.association.properties.thread_id too, and the issue asks to reuse docs(ingest): generate docs/INGEST.md from one ingest contract, with a drift test #5682's generated docs. test_ingest_doc_drift.py does not compare aliases against the code, which is why CI stayed green.
  • Bare thread ids collide across applications. Many LangGraph examples use thread_id="1", so two apps could map to the same session_id. The same was already true for a bare gen_ai.conversation.id, but the fix now also moves the token-bearing child spans onto it. I did not check whether the sessions table keys on session_id alone. If it does, that is a cross-app merge worth an issue.
  • Remote example uses https://clawmetry.internal:8900. The dashboard does not serve TLS itself, and the CI proof used http://. Existing docs (BRING_YOUR_OWN_AGENT.md) already use this pattern, so it is not new. Still, one line saying TLS needs a terminator in front would stop the Bearer token being sent in the clear.
  • "What each tab shows" table. Its evidence is the API endpoints, not a screenshot of the Tracing or Sessions tabs. That is acceptable, but say "checked through the endpoints each tab reads" if that is what was done.
  • Known Guard defect. Guard labels span-only sessions as openclaw and offers controls that cannot act. It is honestly documented, but it should be filed or linked on OTLP traces: spans bypass Guard detectors and redaction #5938 so it does not live only on a docs page.

vivekchand added a commit that referenced this pull request Sep 14, 2026
…cedence

Review on #5972 found the product record and its guard disagreed: AC .4 said
a sent session identifier outranks the thread, while _otel_to_row, the page
and the test rank the thread above session.id. The code is the right side of
that: if a span-level session.id beat the thread, an app that stamps
session.id on every span splits the run again (top span via
gen_ai.conversation.id, children via session.id).

- AC .4 reworded (tracked edit on the Factory requirement, mirrored in
  docs/acceptance_criteria.json): conversation id > thread > session.id on
  the span or resource, the winner recorded as sent.
- The test now covers session.id on the span as well as the resource; both
  cases fail if session.id is read before the thread key.
- The code comment no longer says only a resource session.id is outranked.
- ingest_contract.py lists the thread key among the conversation-id
  fallbacks; docs/INGEST.md regenerated.
- The page states the rule and why, notes the dashboard serves plain HTTP
  (a TLS terminator is needed for the https:// example), and says the tab
  table was checked through API endpoints, not rendered tabs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
@vivekchand

Copy link
Copy Markdown
Owner Author

Review follow-up (the blocking item on AC-OBS-OTR-001.4), fixed in 44d0eb8:

Decision: keep the behaviour, correct the record (option a). The precedence is: sent gen_ai.conversation.id, then the thread, then session.id on the span or the resource. Letting a span-level session.id beat the thread would bring the split back. The top invoke_agent span resolves through its conversation id, so an app that stamps session.id on every span would file the children under a different session.

  • Factory: a tracked edit on AC .4 of REQ-OBS-OTR-001 (https://factory.8090.ai/project/b415065f-ab2f-4f53-8864-0c009fd098cb/requirements/0130f6a6-3f5b-448f-bd59-d8820a4d5c88) is pending review. It is mirrored word for word in docs/acceptance_criteria.json.
  • Guard: test_thread_wins_over_session_id_on_span_or_resource now covers session.id on every span as well as on the resource. With session.id read before the thread key, both cases fail (2 failed, 10 passed). With origin/main's dashboard.py, 5 fail. With the fix, all 12 pass, plus tests/test_ingest_doc_drift.py. check_ac_coverage.py --check is OK.
  • Code comment: it now says the thread outranks session.id at both levels, and why.

Non-blocking items taken:

Not taken: the bare-thread-id collision across service.names. It predates this PR for bare conversation ids, and I have not verified how the sessions table keys it. It is listed as remaining.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 14, 2026
@vivekchand

vivekchand commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner Author

Ready to merge (at 44d0eb8). Every check passes except OpenSSF Scorecard, which is skipped. That includes E2E Gate (required), Drift Bot, the product-record gate, CodeQL and the OTel recipe (LangGraph + OpenLLMetry) job. GitHub reports the PR as mergeable (merge state CLEAN). Not merged.

Merge-after dependencies: none. The branch is on current main (658342f) and does not depend on #5953 (spans reaching Guard) or #5963 (durable ack), so any merge order works.

Companion PRs: none. There is no new HTTP route, so no cloud route-policy entry is needed.

Before or at merge: accept the pending tracked edit on AC .4 of REQ-OBS-OTR-001 in Factory: https://factory.8090.ai/project/b415065f-ab2f-4f53-8864-0c009fd098cb/requirements/0130f6a6-3f5b-448f-bd59-d8820a4d5c88. docs/acceptance_criteria.json already mirrors the new wording.

After merge (on main):

  1. The OTel recipe (LangGraph + OpenLLMetry) job passes on the main push. Its log should show:
    • PASS;
    • a 401 without the token;
    • one session holding both runs on the thread, with 280 in / 27 out per trace;
    • an invoice_agent:trace:<id> session for the run with no thread.
  2. python3 -m pytest tests/test_otel_recipe_langgraph.py tests/test_ingest_doc_drift.py -q passes.
  3. python3 scripts/check_ac_coverage.py --check is OK, and Drift Bot is green on main.

After release: repeat the CI job by hand at the release tag, using a scratch HOME and scratch venvs, never the real ~/.clawmetry.

  1. Install the dependencies:
    • dashboard: pip install "clawmetry[otel]==<released>", plus pytest if you also want to run the tests;
    • recipe, in its own venv: pip install --require-hashes -r .github/requirements/otel-recipe-langgraph.txt.
  2. Start the dashboard from the tag's checkout: HOME=$(mktemp -d) OPENCLAW_GATEWAY_TOKEN=t CLAWMETRY_LOCAL_STORE_PATH=$(mktemp -d)/r.duckdb python3 dashboard.py --port <free port> --host 0.0.0.0 --no-debug &
  3. Run the verifier: python3 scripts/verify_otel_recipe_langgraph.py --url http://127.0.0.1:<port> --token t --agent-python <recipe-venv>/bin/python --remote-url http://<this machine's LAN IP>:<port>. It runs the agent itself (invoke, stream and no-thread). Expect PASS.
  4. Kill the dashboard, then delete the scratch HOME and venvs.

Still open on #5939:

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 14, 2026
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 14, 2026
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 14, 2026
vivekchand and others added 4 commits September 14, 2026 12:36
…try in CI

Refs #5939. REQ-OBS-OTR-001.

Running a real one-tool LangGraph agent (langgraph 1.2.11,
opentelemetry-instrumentation-langchain 0.62.3) against the receiver found
that a run was split in two: the instrumentation sends the thread as
gen_ai.conversation.id on the top invoke_agent span only, and as
traceloop.association.properties.thread_id on every span beneath it. The
receiver read only the former, so the thread's session had 0 tokens and a
per-trace session held every token and the tool call.

_otel_to_row now reads the thread association right after the
conversation id, recorded as sent.

- examples/otel/langgraph/: the recipe (one tool, stub model, no key)
- scripts/verify_otel_recipe_langgraph.py + ci.yml otel-recipe-langgraph:
  hash-pinned recipe venv, dashboard on a non-loopback address, checks
  401 without Bearer, one session for invoke + stream on a thread, exact
  tokens, tool span under invoke_agent, per-trace session with no thread
- tests/test_otel_recipe_langgraph.py: real captured exports through the
  receiver (4 red on main)
- docs/OTEL_RECIPE_LANGGRAPH.md: versions, encoding, auth, structure,
  identity, per-tab behaviour, observe-only limits

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
The first CI run of otel-recipe-langgraph failed at boot: on a fresh store
path the DuckDB file does not exist until the receiver's first write, so
the read-only local API answers 503 "database does not exist" and the
readiness loop never passed. Reproduced locally in a venv installed from
ci-tests.txt + ci-otel-extra.txt; /api/health answered 200 in 3s.

Readiness now uses /api/health like every other job, and the dashboard
runs with -u so a boot failure prints its log. Rehearsed the whole job
locally: verifier PASS including the non-loopback 401/Bearer step.

Refs #5939

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
…cedence

Review on #5972 found the product record and its guard disagreed: AC .4 said
a sent session identifier outranks the thread, while _otel_to_row, the page
and the test rank the thread above session.id. The code is the right side of
that: if a span-level session.id beat the thread, an app that stamps
session.id on every span splits the run again (top span via
gen_ai.conversation.id, children via session.id).

- AC .4 reworded (tracked edit on the Factory requirement, mirrored in
  docs/acceptance_criteria.json): conversation id > thread > session.id on
  the span or resource, the winner recorded as sent.
- The test now covers session.id on the span as well as the resource; both
  cases fail if session.id is read before the thread key.
- The code comment no longer says only a resource session.id is outranked.
- ingest_contract.py lists the thread key among the conversation-id
  fallbacks; docs/INGEST.md regenerated.
- The page states the rule and why, notes the dashboard serves plain HTTP
  (a TLS terminator is needed for the https:// example), and says the tab
  table was checked through API endpoints, not rendered tabs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
The test references this criterion for the recipe-page verification
requirement but it was missing from the manifest, failing the AC
traceability gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUtV6jyUVMhBRWXjSSef9S
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 14, 2026
… Factory

Syntax & Lint failed: manifest has duplicate criterion ids. The later
entry matches REQ-OBS-OTR-001 in 8090 Software Factory verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 14, 2026
Resolves conflicts in:
- .github/workflows/ci.yml: keep both otel-recipe-langgraph job (this branch)
  and selfhosted-server job (main) at same insertion point; each gets its own
  steps block
- docs/acceptance_criteria.json: keep both OTR-001 LangGraph recipe criteria
  and SHI-001-004 self-hosted image criteria from main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5EycwbWKDtGZtoAHVdDSA
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 14, 2026
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 14, 2026
@vivekchand
vivekchand merged commit 946deda into main Sep 14, 2026
50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants