Home - noetl/ai-meta GitHub Wiki

Last refreshed: 2026-10-10 (Claude session β€” ⭐⭐ B3 + D2 are LIVE ON PROD and proven with real data. Server v3.143.0 / ehdb v0.9.0, tick 60 s (not the shipped 15 s β€” chosen from prod's measured 314 appends/hour, where 15 s would carry ~1.3 records per object and write 5,760/day), cleanup default ON. A 32-minute sampler caught the entire chain: 188 unsealed records left the node in 3 batches, objects appeared under tail/ counted externally with gcloud, the age seal fired at ~900 s, and D2 then reclaimed all 6 objects (3 per replica Γ— 2) as a contiguous run of durable parts covered them β€” retained 6β†’0, bucket 3β†’0, tail_failed=0 throughout. ⚠ The first ~400 s read appends=0; that was the window, not the code β€” measured independently, prod had no execution traffic at all. ⚠⚠ The D2 safety property is the design: the watermark stops at the first non-durable part, because max-over-durable skips a local-only part in the middle and would authorise deleting the only off-box copy of those events; both mutants are caught by that one test. ⚠⚠ And a defect I introduced in B3 and caught before enabling: the driver held the engine lock across the remote put, and that lock is taken by shadow_append on the live write path β€” p50 77 ms / p99 234 ms per colliding append, forever; ehdb v0.8.1 splits prepare / upload / commit. βœ… #457 CLOSED on measurement β€” /data 31% β†’ 9%, local 1.2 G β†’ 11.9 M β€” with the one goal item that was NOT done (⚠ 0 of 19 alert policies watch any volume metric) carved out as #466 rather than buried. β›”β›” The prod worker roll is BLOCKED by a broken release, not by its accepted risk, and my first fix was the wrong diagnosis. release-worker failed twice (v6.2.2, v6.2.3), each ~45 min in, on commits whose cargo test --locked was green: noetl-tools = "~4.0" floated to 4.0.6, which needs async-nats 0.47 against our 0.38 β‡’ two incompatible Context types. ⚠ Adding --locked did not fix it β€” cargo chef cook works from recipe.json and writes its own lock over the one copied in, so the cook stage resolved 4.0.6 anyway. The fix is an exact pin (=4.0.1), verified by building the real image (1,008 crates, 0 errors, one async-nats). ⚠ Also corrected: the pools already run v6.2.1, not v6.0.0 β€” I had read build_info off the writer, the only reachable :9090, and generalised it. C2+C3 HELD; election-before-enforce absolute. Β· Earlier today: B3 closes the last in-flight durability gap: the UNSEALED tail now leaves the node. A2/A3 made sealed history survive node loss; a part is local-only until it seals, so the window was the seal interval β€” B2 bounds it and cannot close it. Measured on prod while building: SEAL_MAX_AGE=900 with oldest_unsealed_age=719 and unreplicated_oldest_age=0, i.e. nothing waiting to upload and ~12 minutes of records living on one disk. ehdb v0.8.0 writes the pending records as write-once per-batch objects under tail/ β€” never parts/, or plan_retention could drop them β€” and cold_load_replicated replays them, deduping against the manifest's max_sequence because a part that seals after its tail objects makes them redundant by design. ⭐ Window: 900 s β†’ the tick. Bounded, NOT zero β€” no remote ack gates the local append, so it is neither consensus nor a quorum, and the tick is the number to quote. ⚠ The RED control is the load-bearing test: with the flag off, 16 of 21 records come back, so the loss is real and every green is falsifiable; with it on, destroying the local root and cold-loading from the replica alone returns 21 of 21, and 13 of 13 through a real GCS emulator. ⚠⚠ A defect I introduced in v0.8.0 and caught before enabling anything: replicate_tail performed the remote put while holding the engine lock β€” and that lock is taken by shadow_append inside emit_events on the live write path, so every append colliding with a tick would have waited out the put (p50 77 ms, p99 234 ms, once per tick, forever). A durability feature becoming a write-path latency regression. ehdb's uploader thread had documented the rule the whole time ("the substrate writes happen OUTSIDE the lock so a slow store never blocks appends/reads"); v0.8.1 splits prepare (no I/O) β†’ upload (free fn) β†’ commit (no I/O), pinned by a test whose RED control restores the defect. ⚠⚠⚠ And a correction to my own shipped default: prod's measured rate is 314 appends/hour, so a 15 s tick carries ~1.3 records per object and writes 5,760 objects/day β€” the count is driven by the tick, not the data. 60 s is the better choice (still 15Γ— better than 900 s, a quarter of the objects) and the 15 s default I shipped is likely mis-set. B3 is emulator-proven and READY but NOT enabled on prod: enable needs D1 (tick) and D2 (tail-object cleanup β€” a data-deletion policy on the event log), both owner calls, now with measured numbers on #460. D3 resolved as recommended (a failing tail put fails nothing). C2+C3 HELD; election-before-enforce absolute. Β· Earlier today: the off-box replica was armed on prod and the acceptance proof found the defect instead of rubber-stamping it. A2 reached a genuine second failure domain (domains=[local-device:66320, remote-gcs]; survives_node_loss 0β†’1, single_point_of_failure 1β†’0) and parts sealed from then on replicate β€” the age seal fired at t=934s of a 1853s window and the part plus manifest landed in the bucket, counted externally with gcloud. ⚠⚠ Then the proof refused: read back from the remote's own manifest, 40 parts listed and 39 at replica_count 1. Uploads are enqueued on seal and nowhere else, so arming a replica replicates nothing that already exists β€” and every durability gauge read healthy the whole time because they describe the declared replica set, not the parts. ⚠⚠⚠ Worse than an empty remote: the bucket holds a manifest naming all 40 parts and the bytes of one, so it reads as a complete store and is not a recoverable one β€” which makes the backfill a correctness prerequisite, not an optimization. Fixed rather than redefined: ehdb#400 β†’ v0.6.0 backfill_under_replicated, reading bytes back out of a replica that holds them because durable_view nulls local_path, so a repair path that only knew fs::read would have compiled, run, reported success and copied nothing β€” the same defect one level down. Backfills also carry their own counters: a part sealed in an earlier process has no sealβ†’durable interval, and folding it into upload_lag_micros_total would divide the real total by a larger denominator and deflate a mean a capacity decision reads. ⭐ The worker's v0.3.2β†’v0.5.2 pin bump had exactly one compile break across two minors β€” the new ShardLag.inflight β€” and refusing to default it to 0 was the whole point: render_snapshot publishes that field and high-lag-with-zero-inflight is specifically the stalled consumer reading, so a hardcoded zero makes a healthy bus publish an outage signal on every scrape (ehdb#402 β†’ v0.7.0). βœ… #463 CLOSED β€” vendor/ removed (19,670 files, 572 MB) via server#528. ⚠ Three of my own mutants did not compile before one did, and a mutant that does not compile is not a caught mutant. ⚠⚠ Rule 0a drift found and fixed on this very page: the Active-umbrellas section was missing #457, #459, #460, #462 and #463 β€” every umbrella of the last two sessions, including the one the session was about. C2+C3 remain HELD; election-before-enforce absolute. Β· Earlier (2026-10-09): a retention + object-store archival tier for the event log, built end to end and proven against a real GCS emulator; the hot volume becomes a WINDOW rather than an accumulator (#459, closing #457). Audit first: four things already existed and were not rebuilt β€” the retention planner (plan_retention, drops whole parts below a sort-key floor, never splits a part), its executor (apply_retention), a GCS client live in prod (ObjectBackend/GcsBackend, ADC via Workload Identity, 21,569 objects written) and the archive bucket itself. ⚠⚠ And four that did not: no cloud DurableSubstrate (only LocalFs + InMemory, so nothing goes off-box and prod's "second replica" is the same PVC); apply_retention had ZERO callers in the server β€” built-and-dormant, the #332 pattern; no time- or completion-based floor; no per-execution layout, date index or config. ⭐⭐ The constraint that shaped everything: partition = shard_for_execution with shard_count=1, so a part holds many executions interleaved β€” an execution owns no part, execution_bloom cannot enumerate, and dropping an old part would destroy the early events of a still-running execution. Resolution: archive per-execution (exact), prune per-part (coarse), with a floor as the bridge. P1–P6 merged: config, archive to execution_id=<id>/, a by-date index that is one object per execution (GCS has no append, so a per-day manifest would be read-modify-write and lose entries under concurrency β€” 60 concurrent archives on one date all survive), prune-after-durable, retrieval by id and by date, and the metrics. ⭐ Proven on a real L0Engine: with a live execution unarchived the floor pins at sequence 2, dropped=0, 20 of 20 of its events survive; archive it and the floor rises, dropped=9, parts 9β†’0, 10,736 bytes reclaimed. ⭐ A real bug in my own floor was caught only because the test asserts on reclaimed bytes rather than on the call returning Ok β€” the all-archived branch used the highest FIRST sequence, so a fully-archived log reclaimed nothing while every call succeeded. ⚠⚠ Scope correction: the store this prunes β€” /data/ehdb-embedded, 2.4 GiB β€” is a SHADOW; verified with a control grep that its only readers are the shadow writer and the parity verifier, and GET /api/executions/{id} reads Postgres noetl.event, which is append-only and never purged. So pruning it cannot make API history unreadable, and wiring a fall-through there would have been an instrument that cannot see its own target. ⚠ Blocked on one owner action: the server SA has no IAM binding on the archive bucket, so archiving fails closed; the exact gcloud command is on #459. Flags default-off β€” nothing in prod deletes anything. Β· Later: P7/P8 audited and specced (#460, design only, nothing built). ⚠⚠⚠ PROD RUNS RF=1 AND EVERY DURABILITY SIGNAL READS HEALTHY β€” both "replicas" are on device 66320, yet replica_domain_violations=0: L0Engine::open wraps the substrate in a replica set of exactly one and the check is gated on len() >= 2, so the 0 was never evaluated. FailureDomain::Remote exists and is constructed in tests only, so survives_node_loss is false by construction everywhere. ⚠⚠ The exposure is the unsealed tail, and on the tier it is unbounded in time β€” verified in code: with_seal_max_age is called at exactly three sites and tier_store.rs calls it zero times, so the two buses get the 5 s bound and the store at risk gets 256 MiB and no clock. ⭐ Bounds stated rather than softened: EHDB is not etcd-with-sharding, it is a WAL with a lease (etcd has consensus and no shards; EHDB has shards and no consensus); P8 gives mutual exclusion, not consensus, and reduces write availability under partition; and by CALM there is no coordination-free linearizable cross-shard read, ever β€” a theorem, not a gap. ⭐ The #459 archive is not P7 (terminal+expired only, lagging, downstream of sealing) but it retires the dependency blocker, because DurableSubstrate is a sync trait written on the uploader's dedicated thread, so GcsSubstrate can live in the server and be injected. Measurement phases unblocked; the three semantics changes are owner-gated. Β· A1/B1/C1 now MEASURED (server#511): four always-computed replica gauges seeded pessimistically (an unevaluated set must not read as safe) with the alert keyed on survives_node_loss rather than a count; the tier's active catalog.jsonl sits 7.1 MiB unsealed for ~7 hours, so a byte-only trigger bounds the busy tiers and leaves the quiet ones exposed; and five durable-state mutation paths are enumerated, none wrapped. ⚠⚠ Two of my own framings corrected: the tier is not an L0 engine so with_seal_max_age does not apply (age-based segment sealing would be a new feature β€” stays specced), and I had checked the wrong binary for the fence β€” the writer links ehdb-reference and the fence is already a selectable decorator, so what is missing is the election, not the fence. ⚠⚠⚠ Which sharpens C3: with no election live, enforcing is an immediate outage, so C2 strictly precedes C3. Β· C1 guard landed (worker#348, zero src changes): a second store-construction site or a new unregistered append* now fails a test. ⚠⚠ Three further guards were written and deliberately not shipped β€” the plain bypass and the read-after-wrap are compiler-guaranteed (move semantics; a match data dependency), my mutants would not compile, and a mutant that does not compile is not a caught mutant. ⭐ The ehdb skew is drift, not a correctness risk (FORMAT_VERSION 1 at both pins, ehdb-service/src unchanged) β€” the cost is capability, which is why C2 needs a pin bump first. β›” P7/P8 is now blocked on user decisions (#460) + the IAM grant β€” no further safe autonomous work; prod is 4 releases behind (v3.131.0 live, v3.135.0 tagged) and one deploy delivers the archival tier, P5 retrieval and the A1/B1 instruments. Β· Later (same session) β€” THE ARCHIVAL TIER IS LIVE AND DRAINING ON PROD, and three of the claims earlier in this very line are now STALE. ⚠ Corrections first, because the dashboard contradicting reality is the thing Rule 0a exists for: the IAM grant is DONE, prod is not 4 releases behind (it runs v3.137.2, rolled by digest a3bb8087, rollback c46a032a, 1/1, 0 restarts, env 71 = baseline), and P7/P8 is no longer blocked on no-further-safe-work β€” A2 and B2 both shipped. ⭐⭐ Archival verified with real data: ARCHIVE_ENABLED=true, 1,472 of 6,251 archivable executions archived, every one also verified_durable and indexed, 0 failures, 4,431 real GCS puts. ⚠⚠ But the reclaim number is NOT measured and is not claimed β€” the floor correctly refuses while any archivable execution is unarchived, so enabling prune today would reclaim nothing; reclaim is gated on the drain (~4 h), not on code. ⭐ The GCS-put latency that #460 decision (i) was taken WITHOUT: p50 77 ms, p90 98 ms, p95 139 ms, p99 234 ms, n=300 in-region β€” ~19x a ~4 ms local fsync, which closes synchronous per-record remote append harder than the "tens of ms" estimate did. ⚠⚠ Four defects found by controls rather than reasoning: (1) compute_prune_floor returned NoFootprint when everything is archived β€” the exact steady state the drain produces β€” so the whole prune path would have reclaimed zero while every call returned Ok; caught only by a positive control asserting a fully-archived log DOES reclaim. (2) collect_candidates capped at 200x100 and returned Ok on a silently truncated list, and a floor from a partial population can sit above an execution never classified. (3) The put metric's "pin" was observe(0.0) on a histogram β€” a FABRICATED sample; in prod the only reading below 50 ms was that invented zero, on the very number the decision turns on. (4) My own B2 gauge understated its exposure: it stops at sealing, but replication is async so a sealed part still has a local-only window β€” and unreplicated_snapshot() has computed the whole durability window all along with zero server callers. ⭐ A2 is proved by moving bytes, not metadata: the log cold-loads from GCS alone (6 sealed parts, sequences 1-48, confirmed by listing the bucket outside the test; control bucket 0 objects) β€” a substrate that declared Remote while dropping every upload would pass the unit tests. ⚠ Two GCS buckets are ONE domain, and they do survive node loss, so the gauge that would catch a two-bucket plan is domains_distinct, not single_point_of_failure. ⚠⚠ B2 existed in ehdb and was unreachable from the server β€” no config, no caller outside ehdb's own tests; ehdb's own doc names the trap (a configured seal_max_age with no timer is "inert on exactly the shard it was added for"), so both halves now ship together, guarded including the spawn in main.rs. ⚠ Owner-gated: A3 needs a replica bucket + IAM (exact commands on #460); and PR #355 has 38 commits stranded on a design branch, including the pointer bumps for several prod rolls, now CONFLICTING β€” all four gitlink conflicts measured as "this branch is strictly ahead", STATUS.md needs a human call. ⚠⚠⚠ C2+C3 remain HELD; election-before-enforce is absolute. Releases v3.138.0-v3.140.1 are cut and not deployed.) Refresh cadence: every session that lands meaningful cross-repo work (per agents/rules/wiki-maintenance.md Rule 0a)

Standing direction (2026-06-04). Per memory entry, Python tiers are deprioritized. Forward Rust-only e2e work is tracked under [#54][i54] (Phase F R5). Python pieces stay deployable for backwards-compat on GKE but are NOT a target for new feature work.

Single pane of glass for the NoETL platform. Every active umbrella, every submodule, every release lands here so a single page shows what's in flight, what shipped, what's next.

Convention. This wiki is the cross-repo dashboard. Per-repo wikis (e.g. noetl/server wiki, noetl/ops wiki) document that repo's surface; this wiki documents the system of repos. See Wiki convention for the split.

βœ… Verified state β€” measured 2026-10-08, not read from notes

Everything in this block was read from the live system or the repo in one pass. The Sessions log below is an append-only history where each entry was correct on its date; where it disagrees with this block, this block is current.

Prod

verified value how
server v3.135.0 noetl_server_build_info{version="3.135.0"} on the serving pod (verified 2026-10-09 16:27Z)
digest 231e8eeb pod imageID (rolled by digest; rollback 94bf4efb=v3.131.0. ⚠⚠ v3.135.0 makes prod report replica_single_point_of_failure 1 / survives_node_loss 0 β€” redundancy unchanged, visibility new; and the retention/archival tier is deployed with flags OFF)
health 1/1, restarts 0 pod status; gen=59 observed=59, currentRev == updateRev
env count 67 β€” name-set identical to the pre-roll baseline STS container env
EHDB library v0.5.0 (ehdb-feed + ehdb-l0, both pins, tag=v0.5.0#d61530b2) Cargo.lock on noetl/server@main
EHDB FORMAT_VERSION 1 β€” unchanged across the upgrade, so the layout gate did not refuse the existing store release-ehdb verification build
ehdb-l0 metrics βœ… 34 series scraping, incl. all four C6 state gauges /metrics on the serving pod (0 before v3.125.0 β€” the server never called the renderer)
ehdb_l0_manifest_parts 311 β€” non-zero and read from the live manifest, so the gauge is reading real state rather than pinned-at-zero decoration (~576 KB held, per the ~1.8 KB/part model) /metrics
ehdb_l0_parts_under_replicated 0 β€” the signal that had no reporter now reports, and reports healthy /metrics
D8 runtime registry βœ… REACHED β€” the server self-registers and renews its lease: noetl_runtime_registry_total{registered}=1, {heartbeat}=4, lease age reset 59s β†’ 6s /metrics + GET /api/runtime/topology
discovery surface βœ… GET /api/runtime/topology β†’ 200, live_total=1, Server: noetl-server-rust-embedded-0:2 noetl-server/3.126.0, unknown_timestamp=0 live probe
watch_since βœ… /api/runtime/watch?after=0 β†’ 5 ops {Register:1, Heartbeat:4}; after=5 β†’ count=0, QUIET (a watch, not a scan) live probe
⚠ remaining P-work workers / gateways / EHDB do not self-register yet (discover(Worker) empty); executions unregistered; SecretRef unused grep + live topology
⚠ auth gate mode the /api/runtime/* layer is attached, but NOETL_INTERNAL_AUTH_MODE is absent so the gate is in shadow and rejects nothing β€” same posture as every other gated route (#303) STS env + an unauthenticated 200
⚠ #391 compaction inapplicable by construction β€” the server uses only D1EventLog (does not opt in) and has 0 VectorStore refs, so records_superseded = 0 is the correct reading grep + /metrics
what v3.124.0 actually changed the ToolSpec.params sequence fix β€” it rejected all 53 adiona playbooks, which are now runnable in prod git log v3.123.2..v3.124.0 = 2 commits
chain gates ARMED and working β€” NOETL_CHAIN_{POPULATE,ADVANCE,SOURCE} present in the process; chain_populate{extended}=229 kubectl exec … env + /metrics
chain bad states 0 (diverged / fork / multiple_roots / stale_log) /metrics
eventlog divergence 7, kind=count only, with mirror_attempt{unavailable} pinned at 2 /metrics

⚠⚠ CORRECTED 2026-10-07: there is NO mirror loss, and the shutdown drain already exists and works. I previously wrote that unavailable counted lost batches, "one per roll". It does not β€” it is AttemptOutcome::Retryable, a failed attempt that gets retried. The arithmetic over a 22-hour window closes exactly:

enqueued 11330 = drained 11330,  shutdown_abandoned 0,  queue loss 0
mirrored 11323 + recovered 7     = 11330   (every event reached the tier)
parity   match 1332 / divergent 7 = 99.48% agreement

flush_on_shutdown() (ehdb_eventlog_mirror_queue.rs:449) is called from main.rs:1464 on SIGTERM, and shutdown_abandoned is 0. ⚠ I also attributed unavailable=2 to rolls that happened before this pod started β€” a counter cannot record those. divergent = 7 equals recovered = 7, consistent with a comparator sampling during a retry window (a timing artifact, the #325 family), though counters cannot prove the sets identical. #343 β€” no code change; the residual is comparator tuning, which is owner-gated.

⚠ The chain gates were disarmed at 18:36Z on 2026-10-06 and re-armed at 19:23Z when the "is it blocking?" test returned NO. Net state is the original baseline. #410 is applied and CLOSED.

CI fleet β€” the clippy gate, per Rust repo

All 8 Rust repos gate clippy with -D warnings. All 8 pin the toolchain at 1.99.0. Only cli carries named allows (2: large_enum_variant, result_large_err, justified in #378).

repo clippy step pin
catalog Β· server Β· worker Β· tools Β· gateway Β· ehdb Β· signal-mesh βœ… -D warnings β€” gates, 0 allows 1.99.0
cli βœ… -D warnings, 2 named allows 1.99.0

So #374 (the || true inert gate) and #378 (the allow-list paydown, 23 β†’ 2) both closed correctly, and there is no clippy-gate debt outstanding.

⚠⚠⚠ CORRECTION β€” this table said the opposite earlier on 2026-10-07, and I was wrong.

It claimed "3 of 8 gate clippy; 5 cannot fail (server, worker, cli, tools, gateway)". I measured the local submodule checkouts, which are 7–23 commits behind origin/main, so I read the pre-#374 state and published it as current.

⚠ The irony is the point: this happened inside the sweep whose purpose was killing stale assertions, in the same edit where I told readers to "read the version from the pod digest, never from this table" β€” while reading a workflow file from a stale tree. The lesson generalises past versions: measure a repo claim against origin/<default>, never against a local checkout, which is already recorded as a false-zero source.

⚠ A second, independent error in the first pass: the probe took head -1 of the clippy lines, which for catalog is the cargo clippy --version reporting step and carries no -D warnings β€” so even against a fresh tree it would have misread catalog as ungated. Two defects stacked in one three-line probe.

noetl/catalog

22 PRs merged, 0 open, 142 tests, AC3 holds at exactly 4 Dataset impls. /api/catalog/* is the only interface β€” no SQL surface of any kind. Its wiki repo does not exist yet: catalog.wiki.git returns Repository not found because GitHub creates it only after a first page is saved through the web UI. Content is staged at docs/wiki/Home.md with publish instructions; nothing claims the wiki is live.

Active umbrellas

Opened 2026-10-09, advanced 2026-10-10 β€” the durability frontier: prod ran RF=1 and every signal read healthy (#460). A2 (off-box replica) is now armed on prod against a genuine second failure domain β€” domains=[local-device:66320, remote-gcs], with replica_set_size 1β†’2, survives_node_loss 0β†’1 and single_point_of_failure 1β†’0 β€” and parts sealed from then on do replicate, proven over a 1853s window (stated because a window shorter than the ~900s seal period would have proved nothing) with the objects counted externally by gcloud. ⚠⚠ The acceptance proof then refused to pass, which is the point of having one: read back from the remote's own manifest, 40 parts listed and 39 at replica_count 1. Uploads are enqueued on seal and nowhere else, so arming a replica replicates nothing that already exists, and the healthy gauges describe the declared replica set rather than the parts β€” a claim about configuration being read as a claim about data. ⚠⚠⚠ And the result is worse than an empty remote: the bucket holds a manifest naming all 40 parts and the bytes of one, so it reads as a complete store and is not a recoverable one. Fixed rather than redefined: ehdb#400 β†’ v0.6.0 backfill_under_replicated, whose own trap was that durable_view nulls local_path, so a repair path that only knew fs::read would have compiled, run, reported success and copied nothing β€” the same defect one level down. ⭐ A second defect fell out of the worker pin bump: ehdb#402 β†’ v0.7.0, because ClaimCoordinator could not report in-flight depth and a literal 0 there does not publish "unknown" β€” it publishes the stalled-consumer reading, every scrape, on a healthy bus. Honest framing throughout: bounded-loss consistent prefix of sealed history, not "no data loss." ⚠ C2+C3 (election + fencing) remain HELD; election-before-enforce is absolute.

Opened 2026-10-09 β€” two durability gauges contradicted each other on prod (#462, RESOLVED). Neither lied: unrep_age = unsealed_age βˆ’ Ξ΄ where Ξ΄ is a 1 ms capture skew between writer.append() and on_append, measured in 4 of 8 runs. ⚠ The valuable by-product was catching that the A3 divergence check I had drafted was invalid: unrep_age at a seal boundary is the part's lifetime, not upload latency, so it diverges identically for a local-only replica and would have "passed" with no remote at all. The A3 check is now defined on instruments that were validated (ehdb#399).

Opened 2026-10-09, CLOSED 2026-10-10 β€” vendor/ was 19,670 tracked files consumed by nothing and already stale for 57 of 344 crates (#463, landed via server#528). Removed: 7,331,526 deletions, 572 MB. Build, 1510 tests and clippy verified clean without it, and the repo's one --offline call (ci/stamp-version.sh:47) still succeeds. .gitignore carries the reasoning inline so it cannot silently return.

Opened 2026-10-09 β€” a retention + object-store archival tier, so the hot EHDB volume is a window rather than an accumulator (#459, closing #457). ⭐⭐ Reclaim is banked and measured, not estimated: 2.30 GiB growth-corrected β€” df_used 3,283,072 β†’ 885,496 KiB, manifest parts 329 β†’ 26, PVC 32.1% β†’ 8.7%, 306 parts pruned. The constraint that shaped the design: partition = shard_for_execution with shard_count=1, so a part holds many executions interleaved and dropping one would destroy the early events of a still-running execution β€” hence archive per-execution (exact), prune per-part (coarse), with a floor as the bridge.

Opened 2026-10-07, advanced 2026-10-08 β€” the EHDB north star: a distributed registry and control plane (#455, see Umbrella: EHDB north-star registry): EHDB as "like etcd but truly distributed" β€” append log, catalog relations, topology/service registry with discovery including ephemeral runtime units, secret references (references, never material), and vectors. All internal EHDB, cleanly separate from the external business catalog. P1–P5 are now REACHED in prod (Server, Gateway, Worker, ephemeral Execution, and the secret-material guard); Ehdb is shipped and awaiting its first command dispatch. ehdb v0.5.0 is cut and live, both server pins moved, and the server has shipped v3.124.0 β†’ v3.131.0. ⭐⭐ ehdb#391 CLOSED β€” Dataset::supersede_key lets a merge collapse superseded keys, taking a vector collection from 4,608 stored records / 65.02 ms to 500 / 7.13 ms, converging to exactly the live set. ⚠⚠ But only one of the three named consumers may opt in: RuntimeDataset is disqualified by watch_since β€” the op-log reader added in P2 β€” because one history reader disqualifies a dataset. βœ… ehdb is now releasable (it had no automation at all; every v0.4.x tag was hand-pushed), with GH006 and the bot-tag trap avoided by construction β€” and ⚠⚠ gated OFF behind EHDB_RELEASE_ENABLED, verified by merging it: the job ran on main and was skipped, tag still v0.4.5. βœ… RESOLVED β€” ehdb v0.5.0 was cut, both server pins moved, and auto-release is now ON. ⚠ Auto-release is not auto-deploy: a cut tag does not roll prod; every deploy still takes the full baseline / by-digest / rollback discipline. ⭐⭐ P6 reframed its own deliverable: top_k is an exact brute-force scan, so recall@k is 1.0 by construction and becomes falsifiable only once an ANN index exists β€” while the number that actually bounds the design is cost, which tracks op-log depth, not live points (500 points re-embedded 10x costs the same as 5,000 written once, within 2%). ⚠⚠ Compaction cannot fix it β€” merge combines parts, not keys, and the Dataset trait has no supersede hook at all, so the prerequisite is a key-level compaction primitive (ehdb#391), shared by the D8 registry folds and the catalog folds. ⚠ P7 (unsealed-tail replication) and P8 (writer election + fencing) are specced and deliberately not started β€” both change durability or safety semantics, and a lease without fencing is not safe under partition. ⚠ election.rs/fencing.rs live in ehdb-reference, which the server does not link: existence is not reachability. Two findings filed: ehdb#388 (lazy-ack O(nΒ²)) and ehdb#389 (drain attribution + the #298 cap tradeoff). Still open on the acceptance list: A5, fold determinism has no engine-side enforcement point.

Opened 2026-10-06 β€” the catalog model on EHDB (#427, see Umbrella: Catalog model on EHDB): a generalized, event-sourced catalog of NoETL internal resources, playbooks first, in the new noetl/catalog repo. Storage is EHDB and only EHDB. ⭐ It finishes a named predecessor β€” catalog_log.rs already writes CatalogRecord in shadow and CatalogRelation is a fold nothing calls on a read path. P0 (spec + repo + 4-of-4 RED-proven CI) and P1 (model types, 30 tests) have landed; P2 is the four Dataset impls, where ⚠ the attribute and relation folds must not copy ProjectionStore's latest-wins shape. Spin-off: #429, the kind filter returning a partial set.

Umbrellas open in the ai-task queue: the chain-store repoint blocker (#357 β€” the store cannot tell a genuine chain root from a post-restart one; populator built + kind-proven, repoint ON HOLD), now ARMED ON PROD with the comparator at 100.00% over 772 comparisons after #360 fixed the two defects that failed the first ramp at 6.3% β€” a snapshot behind the store is staleness, not conflict, and coverage is monotonic; the #358 second-writer alarm moved into chain_for, which re-reads the log to tell the two apart, and two server tests that never ran are tracked as #361), request latency on POST /api/execute (#319 β€” the seconds decomposed into layers: the ~4 ms per-append fsync (P1, parked behind ehdb#322), four redundant catalog reads (fixed, server#394), and the mirror's inline fallback (fixed, server#395, v3.100.4 live). See Umbrella: Request latency), the system worker pool ceiling (#318 β€” a fixed 2-shard capacity with no autoscaler; the ClaimCoordinator route is built, the KEDA ScaledObject was deployed, regressed c=6 and was rolled back, and the autoscaler stays held β€” see Umbrella: System Pool Design), the EHDB projection tier (tier 2) (#265 β€” mirror the incumbent snapshot store and make it serve-capable; phase A proven in kind 2026-08-14, 9 arms / 0 failures, NOT flipped and NOT deployed. ⚠ the charter's noetl.projection is a dead table β€” 0 rows, no Rust writer β€” so the real target is noetl.projection_snapshot, which has exactly one INSERT site. ⚠⚠ a 13-event execution completes and gets no snapshot row at all, so the tier can only hold what the incumbent writes and the prod soak must measure coverage, not only divergence), the EHDB primary serve path (#257 β€” a writer-fronted tier service; wiring + reachability slices shipped and kind-gated, no tier is primary anywhere), EHDB-only IaC β€” declared, kind-validated, prod converge pending (#222 β€” automation/ehdb/ via ops#245; the post-NATS topology as noetl playbooks in local mode), the T3 events-bus migration (#212 β€” noetl.events.> off NATS; shadow LIVE on prod, publish + projection parity PROVEN, consume path merged but unproven), the Domain-Specific SLM platform RFC (#139 β€” build + continuously improve org SLMs via NoETL playbooks; travel#63 is the reference implementation), the Rust server parity-port umbrella ([#49][i49]), the distributed-OS program (#107), the event-WAL + derivable-storage model (#104), the decouple-result-data RFC (#115), the worker-driven orchestrate E2E coverage (#111), the Rust regression baseline migration (#98), the Python-era deploy legacy cleanup (#97), the provider-code duplication (#295 β€” after #286 the HotelBeds dispatch code exists in the planner AND the MCP playbooks, with repos/ops a stale third copy; which one wins is an owner decision), and the flights roadmap (#291 β€” multi-city, baggage-aware filtering and fare-rule semantics, none expressible on Duffel offer_requests).

Opened 2026-08-27/28 β€” from the IaC-completeness and secret-migration work: #303 the credential API answers 200 with decrypted data and no Authorization header, reachable from the user worker pool (not internet-exposed; a router-level gate now ships in shadow, rejecting nothing); #306 rbac.yaml declares an unapplied ClusterRole binding the worker SA to cluster-wide delete on namespaces and secrets β€” so the DR procedure itself is the escalation vector (scoped-down proposal in ops#281, left as a draft); #307 projection recovery coverage was ~0 by construction β€” the comparator runs against completed executions and the spine route only holds in-flight ones β€” fixed in server v3.92.0, prod live in verify (folds from the durable tier, digests compared, served_tier still 0; the flip to serve is a separate decision); #305 noetl-cmdbus-writer-1 is 0-replica debris with no IaC holding a bound 20Gi PVC; #304 the prod Postgres password wants rotating; #300 noetl-secret carries an orphan POSTGRES_PASSWORD key nothing binds.

Also open, not yet folded into an umbrella narrative

Rule 0a asks that every open ai-task issue appear here, so that comparing this table against gh issue list --label ai-task is a real check rather than a partial one. These were tracked in their own issues but had no row on this page β€” listed 2026-08-28 after the mismatch was found by running exactly that comparison.

Reconciled 2026-10-02 by running that comparison again: 12 rows were closed issues still listed as open, and 19 open issues appeared nowhere in the live sections. Both fixed β€” the table is now 54 rows and matches gh issue list --label ai-task exactly (0 absent, 0 closed-listed-as-open). ⚠ Measuring this needs the Recently closed / Sessions log / Releases sections excluded and the prose links in Active umbrellas counted: a naive table-row grep reported 84 stale rows and 67 missing, and both numbers were wrong.

Reconciled again 2026-10-04: 60 rows, 0 closed-listed-as-open, 0 open issues absent β€” against 101 open ai-task issues. Two closed rows removed (#385, #211) and two open issues added (#380, #204). ⚠ The slice boundary bit again: cutting the table at the next ## heading swallows Recently closed (it is ###), which read as 139 rows, 81 closed. Bound it on a line-start ### Recently closed β€” ⚠ this very sentence put that string in backticks mid-line, so a plain str.index now truncates the slice to 1,485 bytes and the table reads empty. A note describing a trap can instantiate it. ⚠ And removing the #211 row made #204 absent β€” its only live citation was inside that row's title, so a deletion can silently orphan an unrelated issue. Re-run the comparison after editing, not before.

Issue Title
#422 noetl.event_dead_letter is write-only: once the flag is armed a parked event is the only copy and nothing can list it
#415 Three pinned metric label sets have no guard, so an unpinned label would stay absent and nobody would notice
#410 The serving server pod has been scraped by nothing since 2026-09-09, so six parity/mirror alert families cannot fire β€” manifest fixed, prod apply held
#406 Watch for upstream patches: quick-xml DoS (needs an object_store provider choice) and rsa/Marvin (no fix exists) β€” accepted wait, not a blocker
#204 Build noetl images on Cloud Build into Artifact Registry (additive to GHCR)
#380 Re-bump repos/docs to include the sharded-state-builder RFC once noetl/docs#188 merges
#130 Off-server CQRS drive: per-step transition latency ~1.8–2.3s/hop (off-server state-builder, not the publish-only write path) β€” ~13s for a 4-step playbook
#141 SLM platform β€” Phase B: extract + parameterize the template pack + land platform foundations G1/G2/G3
#142 SLM platform β€” Phase C: continuous-improvement loop + registry/experiment/lineage
#151 keychain kind: secrets / provider: gcp wallet shape does not resolve on Rust runtime
#154 Fix orchestrate ref-not-found infinite re-drive loop + rework SLM shadow off inclusive multi-match fork
#155 Drive Muno planner turn latency from ~22-45s toward instant (eliminate orchestration tax)
#177 Disable Cloudflare Pages native Git integration for travel project (keyless-bundle race)
#180 e2e: unblock container_postgres_init fixture (custom image not in kind)
#181 e2e: unblock tradedb_create_db fixture (absent private IQT submodule SQL)
#182 e2e: unblock tradedb_bootstrap fixture (absent private IQT submodule SQL)
#184 EHDB external driver: outward-facing read access for third-party apps (design/RFC)
#187 B2: migrate e2e/ops DuckDB fixtures off kind: duckdb, then drop DuckDB from the worker image
#195 EHDB boundary: keep the permanent event log (D1) lean; transient processing context bounded + evictable
#196 EHDB boundary: tier command input like results (reference-based in the durable command dataset)
#198 EHDB write-behind sink: transient processing cache β†’ customer system-of-record + eviction policy
#199 EHDB write-behind sink: wire the sink-confirmation signal + auto-sink (follow-up to #198)
#201 Ship the NoETL CLI as a Python 5.0.0 wheel on PyPI (PyO3 over the real command tree)
#207 Untangle noetl/core's imports of the retired Python services (R1-R3 Python-retirement gate)
#227 In-flight executions do not resume after a writer restart β€” they stall and the guardrail re-issue loops
#228 Re-issue idempotency is state-derived, not key-derived β€” it fails exactly when state rebuild fails
#254 Execution status is derived five times in five places; unify it
#256 Playbook registration accepts any tool kind; invalid kinds fail only at execute β€” 6 live paths are dead-since-written
#258 EHDB shadow parity never compares against the authoritative log
#262 EHDB tier read: decide whether one torn record should fail the whole scan or be skipped and reported
#267 Prod deployment manifests omit 84 live env vars: a re-apply would silently unconfigure prod
#283 Apply the noetl.command (event_id) index β€” blocked on a superuser GRANT
#294 Archive the _probe-empty-results playbook from the prod catalog
#296 Rebuilt WorkflowState embedded fold-time, not event-time: replicas diverge on entered_at/completed_at
#299 NOETL_AUTH0_AUDIENCE holds a client id, not an audience
#301 Keychain rationalization: migrate, eliminate, or prune the remaining 18 credential aliases
#309 A DR apply of ci/manifests/noetl/ would overwrite prod with dev specs AND replace the live encryption key
#310 URGENT: live credentials are committed in the public noetl/ops repo β€” rotate
#312 POST /api/postgres/execute runs arbitrary SQL with no authentication
#315 Reconcile poller re-drives a permanently-stuck execution forever: no attempt cap, no eviction, unbounded orch_cache
#322 System pool is killed by liveness 14Γ—/day behind a TCP readiness probe that never withdraws the endpoint β€” the trigger for the #320 mirror loss
#323 Every prod image pin in ci/manifests is behind live β€” a DR apply would roll back past two incident fixes
#325 The two parity oracles disagree by construction: the cross-store comparator compares 3 fields, the fold compares content
#327 Two more noetl.event writers truncate the row: project_events (LIVE, drops parent_execution_id) and events_materialize
#329 Worker adoption of #328: bump noetl-executor 0.5 β†’ 0.9.1 on its own merits
#330 semantic-release publishes struct-field additions as minor: noetl-tools 3.27.0 is breaking
#331 noetl-executor's version is outside semantic-release: publish-crate fails ~2 in 8 releases
#332 Umbrella: EHDB embedded per shard β€” routing, distribution, multi-region
#335 WAL read path double-applies every event: sequential loops can stall (iterations_dispatched)
#336 Spec: 2026-09-11-d3-projection-serve-flip β€” let a behind snapshot serve, make an ahead snapshot unserveable
#338 Hotels return nothing: a stale date literal in hotel-cards, laundered into an empty result
#343 Mirror repair lands only partially: relay transport still fails, and a 1 MiB tier frame cap blocks large executions
#344 EHDB mirror: chunk the tier batch append, then arm it β€” the un-batched fan-out is what times out
#345 EHDB: two executions carry tier records mirrored with a result reference β€” repair cannot clear them
#351 EHDB tier service saturated: ~36s tail reads stall the writer, server and UI
#353 ehdb-selfcheck tier-load reports ok:true for an explicit tier REFUSAL
#354 Catalog page is slow: /api/catalog/list ships every workflow step body
#356 test_validate_weather_loop_outputs needs untracked status.json/event_log.json β€” the last 2 failures on noetl/noetl main
#359 Prod IaC: the server manifest targets a DORMANT Deployment and would create a duplicate-event_id replica if applied
#363 The materializer retries a permanently-unmaterializable event forever: no dead-letter for an FK failure
#364 docs: the blueprint page generates NO heading anchors, so every inbound anchor link to it is broken
#367 chain-certified projections: no profitable consumer on prod β€” measured, and why
#400 A pypi release that forgets the version bump publishes nothing and reports success: --skip-existing plus no tag/version cross-check

Blocked on a human decision (2026-08-05)

Three cutovers were refused this session rather than forced. Each is blocked on something an agent should not decide or cannot safely do:

Issue What is blocked On whom
#234 Repointing google-places, travel/runtime, firestore and the planner off the stale project IAM grant (roles/serviceusage.serviceUsageConsumer β€” the same one as #211) + API enablement (places/maps, firestore, aiplatform) in shastaratech-noetl-prod. ⚠ firestore is a data migration, not a repoint.
#247 EHDB phase 6-10 tier serve-flips β€” and even shadow Host/storage decision: worker pods have no PVC while KEDA scales them 2β†’20. Options are worker PVCs (converts a KEDA-scaled Deployment to a StatefulSet), the writer StatefulSet (durable volumes exist, but replicas: 1 already hosting both buses), or a dedicated tier workload.
#248 Sink-gated eviction, and boundary compaction of ~1.7 GB A code fix (a failed sink leaks an eviction block forever) + ops#246 merged. Compaction needs an RFC β€” no UPDATE/DELETE on noetl.event exists anywhere, by design.

Also open and deliberately unresolved: #246 β€” the measurement half shipped (worker v5.107.0), but whether a payload-level provider error should terminate the step is a semantic decision still to be made; and #169 β€” Auth0 JWT verification stays in shadow until one real login supplies the aud value.

Opened 2026-08-13 β€” #264: the EHDB parity endpoint writes crossstore_divergence_total, the counter its own paging alert reads, so investigating a divergence alert by querying executions inflates it with nothing in the response saying so.

Opened 2026-08 β€” standing platform gaps rather than umbrellas, listed here because each is something an operator would want to know before trusting a dashboard or a manifest: #238 (no alerting exists on prod β€” zero rule objects, zero notification channels, and almost nothing scraped), #242 (four metric recorders with no caller, plus the two production flags that do nothing), #240 (three MCP agents still leak raw provider bodies in prod β€” the source is merged, the catalog is not re-registered), #239 (git worktree on repos/noetl silently reports the wrong directory), #237, #236, #235, and #234 (the pre-migration project is a live dependency of 36 registered playbooks, not manifest drift). #241 (env vars absent from the deployment-spec pages) closed 2026-08-05 β€” all 225 are now documented.

#101 (orchestrator scaling β€” subsumed by the #115 RFC) and #95 (postgres timestamptz NaiveDateTime bug) both closed 2026-06-22. #100 (cursor/claim loop mode) closed 2026-06-15 β€” server v3.8.0 + tools v3.10.1; test_pft_flow_v2 all_passed:true on kind (see Recently closed). #99 (transfer-tool credential aliases) closed 2026-06-14 via tools#65 + worker#87 + e2e#58 (see Recently closed). The subscription / listener tool RFC (#90) closed 2026-06-12 with all 7 phases shipped + live-proven (see Recently closed); refinement follow-ups #91–#94

# Opened Last update Umbrella Status Wiki page
#226 2026-08-04 2026-08-04 PROD: the graceful SIGTERM seal does not reach the events log under load β€” reopened tip regressed 390 records πŸ› οΈ ROOT-CAUSED + FIXED β€” worker#212 + ehdb#312 open, NOT deployed. What settled it was an absence: main wraps the sequence in tokio::time::timeout(15s, worker.shutdown()) and its expiry line never appeared either β€” a slow seal cannot do that, a starved runtime can. seal_and_hold leaked the engine's MutexGuard through exit, which stops post-seal appends by parking every appender on a mutex never released; every ehdb-feed append path takes that std::sync::Mutex blocking from inside an async task, so each parked appender burns a whole tokio worker thread β€” and the prod worker's CPU limit is 2. Under load the runtime had no threads left, the per-host loop was never polled again, and the events log went out unsealed. Idle it completes in 568 ms, which is why every existing test passed. Fixed twice: FeedWriter::seal_and_close keeps the guarantee with a flag set before the lock and re-checked under it, then releases it; and graceful::seal_all phases across hosts (stop all ingest β†’ quiesce once β†’ persist all cursors β†’ seal all engines on one blocking thread), host-at-a-time having been wrong independently. Partial seals now log at ERROR + noetl_worker_shutdown_hosts_{sealed,total}. Regression holds a backlog in flight at SIGTERM: 2 of 2 sealed, both logs reopen at exactly what they acked, clamped=false, 0 records below cursor. ⚠️ P4 (#222) still needs one under-load graceful restart on a real cluster β€” the claim rests on in-process tests. playbooks/223-ehdb-prod-runbook/P2-result.md
#225 2026-08-04 2026-08-04 PROD: events-side consumers stop consuming after an idle period, while every health signal stays green πŸ› οΈ ROOT-CAUSED + FIXED β€” worker#212 + ehdb#312 open, NOT deployed. The events group-claim face (:9104) had set_nodelay and nothing else β€” no liveness at all; the WAL fan-out (:9108) had TCP keepalive but no application-level heartbeat, so it could not tell "writer alive but stuck" from "feed idle" β€” the one case keepalive structurally cannot catch, being answered by the peer's kernel. Neither ever got what #208 gave the command claim face. The sharp part: claim_loop and run_drain_loop_ehdb already had correct redial logic β€” a half-open read simply never produces the Err that reaches it, so no downstream retry could have helped. Both faces now carry keepalive on accept and connect plus the negotiated heartbeat (primitives lifted to the ehdb-feed crate root so all three faces share one definition), backward compatible both ways. noetl_worker_events_consumer_redials_total{face} makes the reattach visible β€” a counter that could not have moved while the consumers were wedged. 7 tests incl. two negative controls asserting a half-open consumer still parks silently with heartbeats off. Prod's 4 wedged executions have aged out; the 9 stuck RUNNING rows are #226's tail loss and were left in place (no cancel endpoint; clearing them means fabricating terminal events into an append-only log). playbooks/223-ehdb-prod-runbook/P2-result.md
#223 2026-08-02 2026-08-03 Sequenced prod runbook for the three gcloud-blocked EHDB workstreams πŸ›‘οΈ P0 RUN (read-only). P1–P5 not run β€” prod untouched, nothing applied. P0 filled every row of the discovery table against shastaratech-noetl-prod and four runbook premises came back wrong: prod runs NOETL_STATE_BUILDER=offserver (not server, and on the server and both system pools, with a live :9108 WAL client); the writer PVCs are shard-indexed (noetl-cmdbus-writer-0-data, …), not the un-indexed names the IaC carried; the gateway is Deployment gateway in ns gateway; and prod runs all three materializer groups, so state_shard_write and the three-group verify_groups were right all along β€” narrowing them would have made verify read a live group as absent. P4c also drops from a live change to an idempotence check: the ScaledObject is already unpaused with a 3d-old HPA and a single EHDB metrics-api trigger, so the #210 signature is absent. P3/P4 then hardened and made copy-run-safe β€” six IaC defaults now carry prod's measured value (ops#245 @ 5e28543), so the P4 line is the P3 line with plan β†’ converge and nothing else changed; each value that moved into a default has a row naming the kubectl command to re-confirm it at apply time. New P4 gate: K get endpoints noetl-cmdbus-writer-0 must resolve. Unchanged from the plan: P1 must precede P4 (the handover drops the writer pod and #211's sequenced seal is what makes it survivable β€” before it the events writer never sealed at all, and that feed is the sole writer of noetl.event); P2 must precede P4; the prod hard-kill can permanently lose noetl.event records, so the non-destructive exposure bound is the default and the SIGKILL is gated. ⚠️ No command-bus fallback β€” rollback is image/topology only. playbooks/223-ehdb-prod-runbook/
#222 2026-08-02 2026-08-03 No EHDB-only IaC: declare the post-NATS topology as playbooks, then converge prod 🟑 Declared and hardened; prod converge still pending. ops#245 (automation/ehdb/, noetl playbooks in local mode) covers the writer StatefulSet + per-shard PVCs + per-shard ClusterIP hosting all nine faces, the server/pool/gateway env with the NATS-era vars stripped, the KEDA scaler on EHDB per-subject lag, and a paired-evidence proof. 2026-08-03: P0 discovery found six defaults that would have broken or misconfigured prod β€” state_builder=server would have switched a running subsystem off, the claim_* names matched no prod PVC, user_pool_container=worker would have aborted the converge half-applied (a wrong container name is rejected, not ignored β€” a strategic merge appends an imageless container), the gateway target missed its namespace entirely, and the writer's memory limit would have dropped 4Gi β†’ 2Gi. All six are now defaults carrying prod's value. A seventh defect surfaced only by running the adoption path on a kind rig rebuilt into prod's shape: kubectl apply's three-way merge preserves the pre-existing app: Service selector, ANDing it with the new pod-name selector the StatefulSet's pod does not satisfy β†’ endpoints <none> behind a 1/1 Running writer with all nine faces up, cutting off server, both pools, gateway and KEDA at once. Forced with a JSON patch, reported in plan, gated on endpoints in status. Kind: plan β†’ intended diff, converge adopts the three pre-existing PVCs verbatim and returns VERDICT: PASS (published == projected == 380, all three group cursors +380 lag 0), second converge a clean no-op (no generation moved, writer pod UID unchanged, 0 restarts). ⚠️ Prod NOT converged. automation/ehdb/README.md
#209 2026-07-30 2026-08-02 EHDB writer: the SIGTERM seal races in-flight ingest, and a crash loses the unsealed tail πŸ›‘οΈ Graceful half FIXED, RELEASED and PROD-PROVEN 2026-08-03 (worker v5.92.0, worker#211 merged ddb4de8). The first attempt did not work: it signalled with Notify::notify_waiters(), which stores no permit while the acceptor registers on its first poll, so a shutdown firing before that poll lost the wakeup and the ingest listener was never closed β€” caught by the release gate, and settled by lsof showing the writer's own acceptor still in LISTEN inside the test process after shutdown.run() returned (24/30 full-suite runs under contention, 0/15 idle). c44ebf2 makes the signal a watch plus an explicit ack after the fd is dropped, so "stop accepting" is confirmed. Prod proof: a controlled SIGTERM sealed hosts=2 in 568 ms β€” listener closed β†’ cursor persisted β†’ log sealed, for the command bus and the events feed; every cursor resumed at its pre-SIGTERM value with clamped=false / replay_records=0. The events writer sealing is the new part β€” it previously never sealed at all, and it is the sole writer of the durable noetl.event log. Prior kind proof: Sequenced, awaited shutdown β€” stop ingest β†’ quiesce β†’ cursor β†’ seal-and-hold. Mid-burst SIGTERM under 120 concurrent execs: tip 7781 with lag 570 in the unsealed region β†’ reopened tip 7781, clamped=false, all 570 survived. Survey found it worse than reported: the seal also raced process exit, and the events writer never sealed at all β€” the sole writer of the durable noetl.event log. ⚠️ Crash half still open, and the number could NOT be measured in kind β€” the exposure is masked because consumption lag (~1100) exceeds seal_max_records (1024), so a healthy low-backlog system is MORE exposed than this rig. Bound stays the deterministic unit test: 0/300 survive with no seal. Needs L0 active-part replay on open. New sibling gap: the KV face is unsealed even gracefully (KvCoordinator exposes no flush). playbooks/220-ehdb-only-kind-soak/
#194 2026-07-15 2026-07-31 EHDB takeover of NATS β€” server-owned push transport + storage-tier cutover 🟒 EHDB-lag autoscaler LIVE on prod (2026-07-31) β€” the user pool scales on ehdb_feed_subject_lag{subject="commands.shared.shard.0"}: 2 β†’ 4 β†’ 8 β†’ 16 β†’ 20 β†’ 2 under 200 real executions, every rescale attributed to that metric by name; 200/200 COMPLETED, 0 dup. Soak 240/240, out_of_order_appends=0. #208 re-verified live under load (mid-burst writer restart β†’ 60/60, 0 publish errors). server v3.58.4 / worker v5.83.0. ⚠️ T5 still NOT safe: noetl_events is live with 4 consumers (3 worker materializers + the gateway SSE) β†’ T3 unchecked; deleting NATS today costs the SPA its live updates and breaks async callbacks. Also ⚠️ the nats-jetstream KEDA trigger was not the harmless no-op the plan assumed β€” 149 messages frozen since the T4 cutover drove the pool to 15 replicas within 40 s of unpausing; removed. Pre-T5 inventory: playbooks/210-t5-readiness/pre-t5-checklist.md. ehdb: L1 Command-Bus Cutover
#166 2026-06-30 2026-07-13 RFC: object-store-backed sharded state_builder (durable fix for the unbounded in-memory WAL index) βœ… Phase 5 ROLLED TO PROD + validated (2026-07-13); only the deferred Leg 3 remains. Phases 1–4 LIVE on prod (bounded WAL index + Feather state-shard writer + cold-load-on-miss + execution-affinity, worker v5.52.0). Phase 5 Legs 1+2 (server v3.51.0) NOW ROLLED TO PROD + validated 2026-07-13 β€” Stage 1 Mode A (NOETL_SHARD_SUBJECT_ROUTE=true + NOETL_COMMAND_SHARD_COUNT=2 on noetl-server-rust v3.52.0 β†’ noetl_command_publish_total{pool="system",route="sharded"} climbing, shard.0.>/shard.1.> flowing); Stage 2 both system-pool workers on per-shard durable consumers (noetl_worker_system_rust_shard0/_shard1), GATE-1+GATE-2 PASS, 0 restarts, old broad consumer noetl_worker_system_rust idle/drained, redirect tax eliminated (Leg 1 server#273; Leg 2 state-shard GC NOETL_STATE_SHARD_GC server#275). Correctness confirmed in prod: server shard-routes every pool==system command under Mode A (incl. system/scheduled_cleanup) β€” the earlier "keep legacy subject in the filter" caveat was WRONG/unnecessary. Leg 3 (P2P warm-handoff) deferred by design β€” sole remainder; #166 OPEN tracking only Leg 3. 2026-07-12: merged the kind validation harness ops#238 (49c0948) + full kind dry-run of the rollout sequence (Mode A + Leg-2 GC dry-run + rollback, all green, baseline reversible) + copy-pasteable prod runbook playbooks/166-phase5-prod-rollout.md. Rollout is human-gated (GKE); agent does not run it. β€” history: πŸ“ RFC opened β€” design only, no code, no prod change 2026-06-30. Root-caused the recurring system-pool OOM/wedge (#163/#161): the off-server state_builder's in-memory WAL chain-index is O(all event history) β€” measured 654 execs / 17,803 events / ~1.28 GiB idle, ~73 KiB/event because IndexedEvent::raw clones the full envelope; terminal-only eviction (no LRU/TTL/byte-cap), + a full 24h WAL replay on every boot. The 768Miβ†’2Gi bump only moves the OOM date. RFC: object store = source of truth for state/event-index shards (Arrow Feather, extending the #104 tier from result bytes to state shards); workers = bounded LRU/TTL/byte-ceiling cache + cold-load-on-miss β†’ O(active working set) memory. Sharding reconciles the two existing hashes (#104 FNV-1a locator folder for object layout; #116 XxHash64 execution-affinity for cache ownership). Slim chain (drop the full-envelope clone) + result_ref URN; stateful vs stateless worker split; failover = cold-load (state stops being a single resident copy that dies with the pod); preserves sole-writer (#103), causal order (#117), expected_head guard (#156), never-scan (#115). Phased + reversible; Phase 1 (bound the existing index + slim chain, worker-only, flag-gated) ships the memory relief first and measures the working set before any object-store latency hits the hot path. Doc noetl/docs#188 (docs/architecture/sharded_state_builder.md, review-only). docs RFC
#139 2026-06-25 2026-06-27 Domain-Specific SLM platform β€” build + continuously improve org SLMs via NoETL playbooks (MLOps-as-playbooks) πŸ•ΆοΈ Shadow rollout Option A built + kind-validated (#153, review-only) 2026-06-27 β€” local-MLX slm_serve + a default-OFF shadow_slm_compare planner leaf (inclusive fork, byte-identical served turn) + slm_replay --shadowβ†’dataset_build flywheel; kind proofs pass; v3 22-turn stratified agreement tool/arg/slot 0.86 / intent 0.82 / widget_type 0.73 / schema validity 100%; PRs ops #222 + travel #78, not merged, nothing on prod (operator turn-on = endpoint + slm_shadow.enabled flip). Prior β€” Phase A in progress β€” #140 dataset_build + eval shipped as generic config-driven playbooks (ops PR #212 + travel PR noetl/travel#73, review-only); config-only proven on a 2nd toy domain; baseline = 100% schema validity + sub-ms floor latency (deterministic floor; OpenAI ceiling deferred on teacher budget). RFC draft otherwise design+tracking only. Generalizes the travel-domain SLM (noetl/travel#63, the reference implementation) into reusable tooling any org uses to (a) stand up a domain SLM by writing one slm.config.yaml + schemas/prompts/seed-data, and (b) keep it continuously improved by a scheduled playbook loop. Three parts: a parameterized automation/mlops/slm/<stage> template pack (org provides config; framework supplies every stage DAG); a continuous-improvement loop (traffic capture β†’ drift β†’ teacher/human relabel β†’ scheduled retrain β†’ shadow-eval vs incumbent + deterministic floor β†’ gated auto-promotion with registry versioning + instant rollback, with Β§4 retrain/promotion gates); and domain-agnostic platform foundations generalizing travel#70/#71/#72 β€” G1 container/GPU job dispatch (#144), G2 long-async (#145), G3 artifact storage + model/dataset/eval registry on #104 (#146), G4 experiment store (#147), G5 lineage (#148), G6 cost controls (#149), + the loop engine (#150). Phases A–D = #140–#143. RFC docs/rfc/domain-slm-platform.md. 8 open productization decisions for the platform owner (RFC Β§10). Umbrella: Domain-Specific SLM Platform
#107 2026-06-17 2026-06-17 Program: Distributed Multitenant OS β€” Server Dissolution β†’ Global Grid The strategic roof over #101–#105. Blueprint names NoETL a distributed multitenant OS (serverβ†’stateless edge; NATS WAL + object store the only durable state; processing = event-driven system playbooks on a sharded grid; foundation for quantum-cloud-hybrid). 5-step path: CQRS cutover (step 1, shadow green) β†’ orchestrator-as-plug-in (#108, done 2026-06-18 β€” drive core fully wasm-resident (#109 closed); the system/orchestrate plug-in compiles to a 0-import .wasm (server#224), runs identically to native in wasmtime (server#225), is seeded into the registry on boot + servable (server#226), and drives the real workload identically live β€” shadow over the 10Γ—1000 PFT, 529 evals 0 mismatch (server#227); worker-driven cutover β€” the drive runs OFF-SERVER on the pool, kind-validated β€” entry/run_state dispatch (worker#113) + apply_orchestration_result (server#228) + the flag-gated scheduler/apply/state-guard (server#229, NOETL_ORCHESTRATE_PLUGIN_DRIVE); simple_python drove startβ†’endβ†’COMPLETED through the worker round-trip. #108 CLOSED 2026-06-18 β€” (c) the default-flip shipped (server#233, v3.28.0, drive default ON) after a clean scale soak; orchestrator-as-plug-in is done; the in-server shadow + wasmtime server dep retired in #110 / server#234) β†’ per-shard WAL β†’ drop Postgres β†’ cross-shard federation. docs blueprint
#115 2026-06-19 2026-06-20 RFC: Decouple result data from context — reference-only schema + one-level event chain + worker-side state builder PROGRAM-SCALE STEP 2 SHIPPED + multi-replica validated 2026-06-20 — execution-affinity write ordering (#116). Step 1 (KV coherence, server v3.38.0) was necessary-not-sufficient; affinity routes every trigger for an execution to the replica that ShardConfig::owns it (non-owner forwards) so the read→advance is atomic per execution → 2+ replicas produce one unforked chain. server#252 → server v3.39.0 5e00d0a (src/affinity.rs; NOETL_EXECUTION_AFFINITY/NOETL_PEER_URL_TEMPLATE/NOETL_SHARD_INDEX_FROM_HOSTNAME, all default off) + e2e#71 66b6e1b (2-replica StatefulSet topology + rig HARD gate). Multi-replica gate-ON kind PASS: linear/loop/fanout COMPLETE, every chain roots=1/dangling=0/walk==total, forwarded_ok proof, never-scan + sole-writer across replicas; single-replica unchanged; 595 server tests + clippy green; baseline restored. Remaining #117: off-server from_events spine ordered by event_id wedges fan-in under a chain-order≠id-order inversion (affinity + high-concurrency fanout) — fix = order spine by prev_event_id walk; linear/loop already reliable. PROD GKE untouched; all affinity flags default off. Phase 1 SHIPPED 2026-06-19 — references-in-state consume side. Worker resolve_context_references made selective (resolve a noetl:// ref only when this command's tool input binds the step's bulk — a path the bounded extracted summary can't satisfy; predicate / scalar / _ref access reads off the summary; worker#117 v5.36.0). Server hydrate_result_references surfaces _ref/_store/_uri on the kept summary + refs_in_state default flipped to true (server#243 v3.30.0). Kind gate-ON: all 9 #113 stalls COMPLETE (max command ctx 412KB, 0 __orchestrate__ event rows, materializer lag 0); the 3 targets bounded (output_select 1.32MB→10KB, storage_tiers 17.4MB→36KB, lease_expiry 201 spinning orch cmds→16). Closed #113 + #114. Off-server-drive prod cutover (#107/#111) unblocked on the size axis; PROD GKE untouched. Phase 2 MERGED 2026-06-19 — one-level prev_event_id event chain (server#244 → server v3.31.0 f5bd4a8 + noetl#667 → noetl ecd16a2; ai-meta pointer afdb365): each noetl.event/noetl.command carries the chain link, stamped at the emit chokepoint from a per-execution chain-head watermark, covering both gate paths + the materializer; chain-correctness proven walkable / 1-root / no-gap / no-scan across 6 gate-ON executions (incl. a real fan-out branch origin), sole-writer + Phase-1 bounded sizes intact, 573 tests + clippy green. Post-merge verified on live kind (both prev_event_id columns present, server image reflects merged code, gate-ON baseline live). Phase 3 MERGED 2026-06-19 — chain-walk state builder (server-side, flagged) (server#245 → server v3.32.0 8338417; ai-meta pointer bumped): the drive rebuilds WorkflowState by walking prev_event_id head→root (in-memory ChainHeads head + (execution_id,event_id) PK lookups, no WHERE execution_id scan) → same from_events (orchestrate-core unchanged; parity by construction), behind `NOETL_STATE_BUILD_MODE=chain_walk event_scan(defaultevent_scan, prod unchanged), event-scan kept as the fallback (cold-head / lag / non-genesis). Gate-ON validated: parity 41/41 MATCH 0-mismatch (tx-isolated), NO-SCAN proven (event_scans_total=0, 40 builds / 1064 PK hops / 0 fallbacks), all topologies COMPLETE, sole-writer + lag-0 + gate rig PASS, 577 tests + clippy green. Self-merged (no classifier block). **Phase 4 KERNEL + FLAG SHIPPED + shadow kind-validated** ([worker#118](https://github.com/noetl/worker/pull/118) → worker **v5.37.0** fef961c+ [server#246](https://github.com/noetl/server/pull/246) → server **v3.33.0**3e6006d): the pool-side state_builderreconstructsWorkflowState from the **noetl_eventsWAL** — a per-execution chain index walksprev_event_id head→root, caching the spine keyed by the immutable chain head + incremental tail-advance + cold-rebuild on miss/restart. A live WAL **shadow** loop (NOETL_STATE_BUILDER_SHADOW, default off) + the NOETL_STATE_BUILDER=offserver
[#49][i49] 2026-06-02 2026-06-14 Rust server FastAPI parity port β€” primary server v3.6.0 (system worker pool + cleanup/purge endpoint, server#193); prod is Rust-only (server-rust + worker-rust + system pool). Remaining entangled refactor tracked in #97. Umbrella: Rust Server Port
#104 2026-06-16 2026-06-23 Event WAL + derivable result storage: NATS-as-WAL, logical-URI naming, Feather tier In progress β€” πŸͺΆ OQ5 Option A SHIPPED 2026-06-23 β€” producer-staged result tier (worker v5.46.0, NOETL_RESULT_PRODUCER_STAGE default-off) decouples the over-budget tier write from result_store; result_store-retirement soak LIVE on prod β€” T0 2026-06-23T21:45Z (48h floor / 72h target): both worker pools on v5.46.0 + PRODUCER_STAGE=true; the original mint_authoritative{legacy_fallback}=0 gate was vacuous (MINT unset on worker-rust) so it was repointed to resolve_total{fallback_*}=0 + materializer_skip_exists>0 + staged>0 (ops#207), and a GMP rule-label fix noetl.io/*β†’noetl_io_* (ops#208) restored evaluation of both this gate and the #103 flip guardrail; ai-meta ops pointer 8940fd5. βœ… Phase D ENABLED LIVE on prod 2026-06-23 β€” staged A–D enablement complete. NOETL_RESULT_MINT_AUTHORITATIVE=true on system-pool + server only (NOT worker-rust β€” would spawn a stray result materializer at the wrong cell; its consume already resolves from the tier via Phase C): the URNβ†’Feather/GCS tier is now the authoritative result store with noetl.result_store kept as the reversible dual-write fail-safe. Validated across 4 over-budget execs β€” Feather object only at env=prod/cell=usc1-a (no stray), result_store_dual_write_total 1:1 with execs (+ result_store row each time), resolved_feather correct (1200/1100/test_passed=true), event-mat sole-writer projected==acked, lag 0, never-scan 0, 0 restarts, 0 auth errors. LEFT ENABLED, revert armed (set env … NOETL_RESULT_MINT_AUTHORITATIVE-); GC/DR stay OFF. Remaining: OQ5 byte-source re-plumb + result_store retirement (gated). Prior β€” Phases E + F MERGED 2026-06-23 (the FINAL build phases; all PRs squash-merged in dependency order; flags default-off β†’ inert in prod). Phase E β€” side-effect durability barrier: tools#78 publishes noetl_tools::registry::kind_is_side_effecting (noetl-tools v3.17.0 1d49dd5; members noetl-directives + noetl-locator re-published first per the workspace publish order), worker#130 the consume-pool barrier (NOETL_SIDE_EFFECT_BARRIER, adopt-only β†’ side effects fire exactly once across re-drive) β†’ noetl-worker v5.44.0 d696f7e, e2e#79 rig. Phase F β€” result-tier GC + DR: server#264 conservative dry-run-first GC sweeper (NOETL_RESULT_TIER_GC, never deletes a live-referenced object) β†’ noetl-server v3.44.0 341b614, worker#131 materializer verify-and-repair DR (NOETL_RESULT_TIER_DR, byte-identical re-derive) β†’ noetl-worker v5.45.0 dd07016, ops#205 + e2e#80. worker#130/#131 depend on the published noetl-tools 3.17 (no patch/path/git dep). ai-meta pointers bumped. A–F BUILD phases now complete β€” only operational items remain on #104 (prod GCS infra, OQ5 byte-source re-plumb, staged prod enablement/minting cutover). #104 stays OPEN. Prior β€” Phase D MERGED 2026-06-23 (all 4 PRs squash-merged in dependency order server β†’ worker β†’ ops β†’ e2e; flags default-off β†’ inert in prod): the minting flip ships the dual-write window β€” NOETL_RESULT_MINT_AUTHORITATIVE makes the URN β†’ Feather/GCS tier the authoritative result store (materializer = authoritative tier writer, resolve-by-URN = primary consume path) with noetl.result_store kept as the reversible dual-write fallback. No new crate publish β€” no Cargo.toml dep change in either PR; server resolves noetl-locator 0.1.1, worker resolves from the registry. semantic-release cut noetl-server v3.43.0 + noetl-worker v5.43.0. server#263 β†’ noetl-server v3.43.0 6f6b9ef; worker#129 β†’ noetl-worker v5.43.0 be6863a; ops#204 b19b759; e2e#78 07e85aa. ai-meta pointers bumped. OQ5 β€” result_store retirement β€” DECIDED metric-gated (drop dual-write once mint_authoritative_total{path=legacy_fallback} holds 0 across a staging soak + retention time floor), gated on a not-yet-done byte-source re-plumbing prerequisite. Prod minting cutover (rolling server 3.43.0 + worker 5.43.0 to GKE) is a separate next task. #104 stays OPEN (Phase E/F + minting cutover remain). Prior β€” Phase C MERGED 2026-06-23 (all flags default-off β†’ inert in prod): the result-DATA read half β€” server GCS object backend + cell-endpoint registry + GET /api/internal/cells, worker resolve-by-URN read path (closes OQ6) + fixes B/B1; server#262 β†’ noetl-server v3.42.0 c2d5ca9, worker#128 β†’ noetl-worker v5.42.0 7971041, e2e#77 39dc880. Prior β€” Phase B MERGED 2026-06-22 (flag default-off β†’ inert in prod until a future rollout enables NOETL_RESULT_MATERIALIZER_ENABLED): the shadow Feather result tier β€” a separate noetl_result_materializer consume-loop on the system pool writes the over-budget result (tabular β†’ Arrow Feather, non-tabular β†’ JSON, small β†’ inline no-op) to the derived Β§7 key alongside noetl.result_store; nothing reads it until Phase C. All 5 PRs squash-merged in dependency order; noetl-locator member bumped 0.1.0β†’0.1.1 pre-merge so the additive ResultCoordinates::parse/from_locator API publishes. tools#77 β†’ noetl-tools v3.16.0 7da39d8 + noetl-locator 0.1.1 on crates.io; server#261 β†’ noetl-server v3.41.0 4a6659e (sibling consumer); worker#127 β†’ noetl-worker v5.41.0 4b1c15b (consume-loop); ops#203 c92753c (flag + cell seed); e2e#76 04c3332 (kind rig). No downstream repoint needed β€” worker resolves noetl-tools ^3.14.2 + a self-contained local inversion; server resolves noetl-locator ^0.1.0; both from the registry, no git/branch dep. #104 stays OPEN (Phases C–F remain). Prior β€” Phase A MERGED 2026-06-22 (flag default-off, inert in prod until a future server rollout): slim dependency-free noetl-locator 0.1.0 extracted + published to crates.io (resolves OQ7 β€” control plane parses the canonical result URI without noetl-tools' heavy graph; duckdb/kube/arrow/tonic/rhai/gcp_auth confirmed absent from the server tree; noetl-tools re-exports it), and the server accepts the canonical URI behind NOETL_RESULT_URI_ACCEPT. tools#76 β†’ noetl-tools v3.15.0 dc0c5d8; server#260 β†’ noetl-server v3.40.0 c89d078 (dep repointed gitβ†’0.1.0 pre-merge, 623 tests green); e2e#75 rig eeca8b7. Prior β€” RFC upgraded for review (2026-06-22): now that the event-WAL half is live on prod (materializer sole writer, off-server chain-walk, prev_event_id, off-server CQRS cutover v3.39.5), the RFC is re-scoped to the result-DATA half β€” move result bytes off Postgres noetl.result_store into a derivable-URN-addressed Arrow Feather tier in object store, add the resolve-by-URN read path + cell endpoint registry, place the durability barrier at side-effecting tool boundaries. Decided/proposed/open separated; phased plan A–F; #104 is on the critical path for #107 steps 4–5. Review requested on #104. Prior: off-server-drive Γ— gate reconciliation PROVEN (2026-06-19): gate-ON (PUBLISH_ONLY=true) with the off-server drive (PLUGIN_DRIVE=true) + materializer sole writer is now green on kind (the combo #103 left unproven) β€” fresh exec + cursor fan-out β†’ COMPLETED, server wrote 0 noetl.event rows (all PUBLISHED), materializer materialized all exactly once (rows==distinct ids, 0 dup), read-your-writes held via the relocated trigger. Server #238 β†’ v3.29.2 rebuilds WorkflowState from the durable log on cold-cache apply (crash-recovery, kind-proven cold_rebuild); committed e2e rig kind_validate_orchestrate_gate.sh (e2e#61). This unblocked the #103 flip (the 2 cancel/finalize sites are now also done β€” server v3.29.3 #240; #103 is FLIP-READY). Prior: naming foundation noetl_tools::locator (tools#68/#70, v3.12.0) + worker URI stamp (worker#99); blueprint (docs#180). Umbrella: Event WAL Storage
#98 2026-06-14 2026-06-14 Grow the Rust regression baseline: migrate Python-era e2e fixtures Snowflake key-pair JWT validated β€” the last external-tool gap is closed. tools v3.9.2 (tools#62/#63/#64); create_sf_database + setup_sf_table COMPLETED via JWT on kind. Transfer step deferred to #99. Core green: 64 fixtures. β€”
#97 2026-06-14 2026-06-14 Retire remaining Python-era deploy legacy (manifests, kind automation, helm chart) Open β€” Todo. Python manifests, kind redeploy automation that hardcodes Python deployment names, stale helm release rev 185. β€”
#111 2026-06-18 2026-06-19 E2E: worker-driven orchestrate topology coverage + server-API-only gap tracking In progress β€” three committed kind rigs now: kind_validate_orchestrate_offserver.sh (e2e#59) asserts the off-server topology (gate-off), kind_validate_orchestrate_gate.sh (e2e#61) asserts it composes with the PUBLISH_ONLY gate, and kind_validate_cancel_finalize_gate.sh (e2e#62, 2026-06-19) dual-mode-asserts the ExecutionService cancel/finalize writers honour the gate (gate-off byte-identical INSERT; gate-on PUBLISHED, materializer sole writer, terminal state, 0 loss/dup) β€” the rig that closed the last #103 flip blocker. Off-server rig live-green (COMPLETED, __orchestrate__ in noetl.event = 0, dispatched=applied, shadow metric absent). Durable home for the server-API-only gap (server still sole-writer + rebuilds state β€” moves under #103/#104) + two operator decisions: (A) retire in-process drive fallback (gated on prod adopting a post-#108 image; prod still pre-#108), (B) reap accumulating __orchestrate__ PENDING delivery rows in noetl.command. β€”
[i43]: https://github.com/noetl/ai-meta/issues/43
[i49]: https://github.com/noetl/ai-meta/issues/49
[i77]: https://github.com/noetl/ai-meta/issues/77
[i78]: https://github.com/noetl/ai-meta/issues/78
[i79]: https://github.com/noetl/ai-meta/issues/79
[i80]: https://github.com/noetl/ai-meta/issues/80
[i54]: https://github.com/noetl/ai-meta/issues/54
[i61]: https://github.com/noetl/ai-meta/issues/61

Recently closed (last 7 days)

# Closed Title
#402 2026-10-04 The patchable half of the Dependabot-vs-RustSec gap is closed on the published artifact. noetl 5.1.1: shipped-lock RustSec 5 β†’ 3 (h2 0.4.19, rustls 0.23.45). ⚠⚠ The number is only a result because the previous release's lock was scanned first as a positive control and reported exactly the five. Remaining 3 are accepted waits, not blockers β€” #406. ⚠ On the same lock: Dependabot 0 open, RustSec 3 β€” a clean Dependabot page is not a statement about the Rust dependency graph.
#390 2026-10-04 arm64 is built every release, not back-indexed. The honest option rather than the cheap one: build-deb is a 2-entry matrix on ubuntu-24.04-arm, so newest arm64 moved 2.8.7 β†’ 5.0.3. Live repo after: .debs in no index 22 of 23 β†’ 0 of 26, [amd64] β†’ [amd64, arm64], Release fields 1 β†’ 8, self-entry gone. ⚠⚠ Latent and never reported: without --arch, every .deb lands in one index β€” it would have served arm64 binaries to amd64 clients the moment arm64 existed. Promoted REPORTβ†’FAIL (apt#3), each check proven able to fire.
#398 2026-10-04 All 7 Rust repos pin 1.99.0. Cost was the 2 lints measured in advance (cli needless_borrow; gateway result_large_err, kept as a scoped #[allow] because axum's middleware signature makes the error variant a whole Response). ⚠⚠ The pin found what the measurement had not: gateway would not build β€” base64-compat declares [lib] name = "base64" (E0464), referenced nowhere. I called it a local artifact because it reproduced on both local toolchains; CI proved the location real.
#395 2026-10-04 PUBLISHED noetl 5.1.0 β€” the shipped wheel now carries the patched crates. Verified uploaded, not skipped (Skipping 0, already exist 0; PyPI 5.1.0 absent before, 4 files after) and the four crates confirmed three independent ways. ⚠⚠ twine --skip-existing + no tag/version cross-check would have published nothing and gone green (#400). ⚠⚠ Closure is scoped to the 7 tracked alerts β€” RustSec finds 5 more in the same lock (#402).
#378 2026-10-03 Clippy allow-lists 23 β†’ 2 and all 7 Rust repos pinned. server 14β†’0, worker 7β†’0; the 2 left are cli's and are justified by measurement (boxing ToolSpecDoc::One makes schema_guard fail). ⚠⚠ server's -A dead-code was hiding #361 β€” and the entry hiding the cause sat adjacent to it. ⚠⚠ Pinning found ehdb's main already queued to fail on a 1.99.0 deprecation with no commit at fault. Residual: #398.
#361 2026-10-03 Two server tests carried no #[test] and had never run β€” armed, and both pass. Found because #378 removed the -A dead-code that was suppressing the report. ⚠⚠ A stray #[test] above a doc block bound BOTH attributes to the earlier function (duplicate_macro_attributes, also allow-listed) leaving the next one inert β€” the two entries that hid it sat adjacent. An allow-list hides the next finding of the same kind.
#385 2026-10-03 All 7 Rust advisories cleared; open alerts on noetl/noetl = 0 (control-verified: state=fixed returns 290). Arc 214 β†’ 31 β†’ 7 β†’ 0. ⚠ But the published wheel still embeds every one of them β€” pypi.yml publishes only on a pypi-v* tag and none was cut, so Dependabot's 0 describes the repo, not the artifact (#395).
#375 2026-10-03 Every repo that can host a PR gate now has one. ops (#320), e2e (#93) and apt (#2) had none; the 15 wikis cannot host Actions, so a scheduled ai-meta sweep covers them (#392). Each gate measured green first, prints its denominator, and fails below a population floor. ⚠⚠ the sweep's first version printed FAIL and exited 0.
#374 2026-10-03 clippy now GATES on all five Rust repos β€” the `
#221 2026-08-03 An unset internal event-bus source no longer selects the deleted NATS transport β€” SHIPPED + IN PROD. from_env_value(""), default() and a typo like ehbd all resolved to Nats, a transport deleted 2026-08-01, and the failure was silent: the worker registers, heartbeats, reports ready, and its group cursor never moves. Now a hard error resolved on the startup path for all four internal consumers (two had been resolving inside their spawned task, where an error was only a tracing::error! and a dead materializer). Released as worker v5.92.0 (worker#211 ddb4de8) and deployed to shastaratech prod; the P1c precondition check confirmed all four consumers on both system pools already carry _SOURCE=ehdb, so nothing crashlooped β€” 5/5 pods up on the new digest, 0 restarts. Carve-outs untouched: the user-facing nats tool kind and state_builder's user-endpoint paths.
#210 2026-07-31 User-pool KEDA autoscaling restored and moved onto the EHDB bus β€” LIVE on prod. The pool had had no autoscaling of any kind since 2026-07-26 (KEDA 2.15 deletes the HPA while paused is set, so the object looked configured while nothing watched it). Now scales on ehdb_feed_subject_lag{subject="commands.shared.shard.0"} β€” 2 β†’ 4 β†’ 8 β†’ 16 β†’ 20 β†’ 2 under 200 real executions, every rescale attributed to that metric by name, 200/200 COMPLETED / 0 dup; soak 240/240 with out_of_order_appends=0. ⚠️ The nats-jetstream trigger was not the harmless no-op the plan assumed β€” 149 messages frozen since the T4 cutover drove the pool to 15 replicas within 40 s of unpausing; removed (ops#244).
#206 2026-07-31 out_of_order_appends exposed on the command-bus writer :9102. The #203 delivery-loss detector had been asserted in ehdb's tests since the fix but was unobservable in production β€” inferable only after the fact from delivery accounting in noetl.event. T5 removes the fallback bus, so it had to become a gauge a soak can watch, not a postmortem query (worker#198, worker v5.83.0).
#203 2026-07-28 EHDB command bus: records ingested into the writer feed but never delivered to a claimer (~10% loss under load, feed lag=0) β€” FIXED + PROD-VALIDATED. Root cause: the producer (noetl-server) assigned each feed record's sort key from the command's snowflake event_id; snowflake ids are sparse, so under concurrent publish a lower id could reach the single writer after a higher one. The follower cursor had already advanced (ChangeFeed::poll sets cursor = max sort_key read), refill reads strictly > cursor, so the late record never entered pending and was never delivered β€” and because lag() is cursor-relative too, a record below the cursor was never counted, making the loss silent. PartWriter::append had never enforced the ascending-sort_key contract the Dataset API promises. Fix: the writer assigns the ordering key (append_writer_assigned) β€” the only component that can guarantee it (ehdb#300 β†’ ehdb d4b6235 β†’ worker v5.81.1 + server v3.58.1). Kind before/after, identical 40-concurrent burst: 17/40 (23 permanently stuck) β†’ 41/41 (0 stuck); exactly-once 335=335, pod-kill redelivery 25/25. Then prod-validated in the successful T4 full flip. New out_of_order_appends counter β€” not yet on :9102, tracked as #206.
#202 2026-07-28 EHDB L1 T4 canary blocker: per-shard user-pool split vs collapse command-bus to 1 shard β€” DECIDED (Option 2), executed, prod-validated. The EHDB bus partitions every command by execution_id including the shared pool, but the user pool is a single deployment claiming from a single writer β€” on the 2-shard prod that would strand roughly half the shared commands. Option 2 chosen: collapse the command-bus axis to one shard. The two sharding axes were proven independent in code first (sharding.rs:172 "correctness never depends on affinity"; the command bus reads only NOETL_COMMAND_SHARD_COUNT, the state materializer only NOETL_RESULT_SHARD_COUNT), so #166 state sharding was never touched. Kind-validated on a prod-mirroring rig (0 dup across 5 competing consumers, redelivery 8/8 0-loss) β€” which also caught three config bugs before prod (shared NATS consumer or double-delivery; writer NATS_STREAM; a writer in ehdb mode also needs NOETL_COMMAND_BUS_CLAIM_ADDR) β€” then prod-validated in the successful full flip. The per-shard user-pool split was not needed and not built.
#189 2026-07-14 NoETL cloud provider tool (kind: provider) β€” all 5 rounds DONE, umbrella CLOSED. Terraform-style infra management via NoETL playbooks (local mode). R1 Google REST MVP (3.21.0) β†’ R2 apply-mode converge + LRO + local-mode CLI dispatch (3.22.0/cli 4.14.0) β†’ R3 confirm-gated destroy + EHDB ownership projection + endpoint override (3.23.0/cli 4.15.0) β†’ R4 reconcile policy report/enforce/adopt + confirm-gated drift adoption (3.24.0/cli 4.16.0) β†’ R5 noetl provider {plan,drift,orphans,adopt} CLI verbs + wire-shape eventlog-tier fold adapter + stack-scoped ownership (noetl-tools 3.25.0 [tools#90] / noetl-cli 4.17.0 [cli#68]), crates.io-tarball + released-binary verified. Final fork settled by ADR (2026-07-14): keep the raw eventlog-tier fold as the SUPPORTED ownership mechanism; the first-class provider.resource.converged event type + curated projection column is REJECTED β€” it would carve out #178's secret-free-by-construction invariant (the desired spec is credential-free but payload-derived). Revisit only if the raw-tier fold's cost measurably bites (fold latency at scale / tier retention-GC reclaiming needed facts / raw tier unavailable to the query path). Separate defect tracked on its own: #191 (the 3.23/3.24 broken fixed-path extractor). ai-meta d4a4df06 + 56a9b82e + 2a343875.
#171 2026-07-02 Guardrail: orphaned-command sweep SHIPPED + LIVE on prod (server v3.50.0). Server-side flag-gated sweep terminates dead-worker-orphaned RUNNING execs append-only (playbook.failed) once the owner is absent/stale in noetl.runtime — fixes the recurring worker-roll zombie re-drive. server#271; ghcr→AR server-rust:v3.50.0; NOETL_ORPHAN_SWEEP_ENABLED=true. Prod-proved LIVE: a deliberately-orphaned muno/ exec (worker killed mid-call_duffel_offers, no NATS redelivery) swept to FAILED at grace(180s)+one tick, {candidate=1,terminated=1} idempotent 0-error 0-collateral; a pre-ack orphan instead auto-recovered via redelivery. 0 restarts, login 0.4s. Rollback = flag off → v3.49.0.
#167 2026-07-02 Gateway auth client-timeout decouple SHIPPED + LIVE (travel#90, bundle index-CCFdWtGQ.js). SPA gateway-auth client timeout 15β†’40s + one silent retry so a slow-but-succeeding auth drive no longer locks the user out; complements the structural cure #168 (already live). Shipped with #91's slow-turn robustness (friendlyTurnError + checkPlaybookAccess timeout/retry + Retry button). Verified on prod (login healthy + Paris turn renders place_list, no regression). Rollback = prior bundle index-Buo8vFKk.js.
#162 2026-06-29 Muno HOTELS bookable end-to-end on prod — planner v58 + HotelBeds MCP v4 (book_hotel). Hotels were search-only; added a Book CTA carrying the search-time rate_key to mcp/hotelbeds.book_hotel (APITUDE checkRate→bookings, TEST/SANDBOX-guarded — api.test.hotelbeds.com, refuses any non-test host) + a new hotel_confirmation widget. Registered live (latest-wins): MCP v4 (cid 660409660586853242, backward-compatible) + planner v58 (cid 660417115383399292). Verified on prod (muno/hotelbook-*): real TEST booking refs 197-14375473/74/76 (De la Jatte, Paris, CONFIRMED, env=test) incl. live-path → hotel_confirmation; plain hotel search + "Trip to Paris" + login all healthy; 0 core restarts. Rollback = re-register planner v57 cid 660221263901623160. ops#228 + travel#87; refs #155.
#134 2026-06-26 Gateway session-validate 404 → Muno (travel) auth unblocked on prod. The Python→Rust cutover left /api/auth/session/validate unimplemented on noetl-server-rust; on a NATS session-cache miss the gateway POSTed that dead route → 404, rejecting every cache-missed Muno sign-in since ~01:54 UTC. No config toggle existed (auth_playbooks.validate_session was vestigial) → gateway code fix: validate cache-missed sessions by executing the auth0_validate_session playbook via the worker→callback mechanism (mirrors the live check_access); flag GATEWAY_SESSION_VALIDATE_VIA_PLAYBOOK (default true, =false → legacy REST). gateway#31 → v3.4.1 (8abef26) + e2e#83 (36dced2) merged; playbook prod catalog v87, check_playbook_access v88. Prod gateway ns runs the same-code fix image (fix-validate-20260624 @sha256:90dc6cec, 18h/0 restarts; v3.4.1 CI image is ghcr-only). Core/OQ5 soak untouched; 0 restarts. Live proof: 0 session/validate 404s over 18h + probe POST /api/auth/validate {bogus} → 200 {valid:false} with playbook executed + callback delivered. ai-meta ec0fa35. Sibling of #133.
#95 2026-06-22 postgres pg_value_to_json returned null for tz-naive timestamp (NaiveDateTime) + date/time/uuid/numeric/bytea β€” FIXED + shipped to prod. The Rust postgres tool probed i64/i32/f64/bool/String/json/DateTime<Utc> and fell through to Value::Null for everything else; a plain timestamp column hit the fall-through and serialized to null even though the value was present (the auth0_login expires_at: null repro that tripped the gateway's validation). Added arms for timestamp (ISO-8601, no offset), date/time, uuid, numeric/decimal (exact decimal string via lossless binary-wire decode), bytea (base64). tools#75 β†’ noetl-tools v3.14.2 6d9b674; worker#126 pin bump β†’ noetl-worker v5.40.5 da24952. Built prod noetl-worker-rust:v5.40.5 @sha256:45212dbe… (Cloud Build us-central1) + rolled by digest onto noetl-worker-rust + noetl-worker-system-pool (server stays v3.39.5, CPU 250m/2 kept) β€” rolling restart clean, off-server cutover stayed healthy (materializer sole-writer lag 0, command lag 0, WAL rehydrated per #119). 409 tools tests + clippy + a kind-gated live-postgres before/after test.
#103 2026-06-22 Step 2 β€” CQRS event log β€” DONE. Off-server CQRS cutover is live + validated on prod (server v3.39.5 PUBLISH_ONLY=true+STATE_BUILDER=offserver / worker v5.40.x Rust in-process materializer src/materializer.rs as sole noetl.event writer, replacing the Python system/event_materializer playbook). Prod-scoped regression validator landed via e2e#74 (e2e@1deadf1) β€” 28/30 PASS against live prod (2 non-PASS = pg-cred prod-env diffs, not cutover bugs). Superseded Python ops PRs #189/#190/#192 closed in the same pass.
#102 2026-06-22 Step 1 orchestrator throughput β€” DONE. Batch noetl.event INSERTs shipped (server#198 v3.10.0 + server#199, v3.15.x); Part B's worker-side batched event emission was superseded by #103 (worker publishes granularity-preserving events to JetStream instead of HTTP-POST), now live on prod. No residual.
#101 2026-06-22 Orchestrator scaling β€” DONE. Acceptance met + kind-validated: incremental OrchStateCache, server-side hydrate_result_references, env-configurable worker inline budget NOETL_EVENT_RESULT_CONTEXT_MAX_BYTES. Remaining consume side (worker render-time ref resolution) was reassigned to #115 Phase 1 β€” no residual on this issue.
#127 2026-06-22 Batch PFT throughput plateau β€” CLOSED: the task_sequence per-sub-task context optimization shipped to prod, compounding with the CPU-limit bump. Two compounding levers landed against the same hot path: (1) the prod worker CPU limit raised 1β†’2 (applied live earlier this session β€” removed the 38–47% CPU throttle); (2) this change β€” the behavior-preserving per-sub-task context optimization. The task_sequence drain rebuilt the template context per sub-task (running_ctx.clone() + 2–4Γ— to_template_context() deep-clones + per-block ExecutionContext clones + a fresh context_to_value() per templated field). noetl-tools v3.14.1 (tools#74, squash 9dd9aa6 β†’ release c8656c1, published to crates.io): TemplateEngine::render_value builds the proxied minijinja context ONCE and threads it through the recursion (render_value_with/render_with; minijinja Value is Arc-backed β†’ reuse is a refcount bump), and new build_context_with_overlay(&variables, overlay) builds straight from &variables + a small overlay, skipping the intermediate to_template_context() HashMap deep-clone + per-block ExecutionContext clones in the set/policy paths. Isolated micro-bench (CPU held constant): per-sub-task context cost 2988.9Β΅sβ†’1147.1Β΅s (βˆ’61.6%, 2.6Γ—); 407 lib tests + 2 new equivalence pins + clippy clean. noetl-worker v5.40.4 (worker#125, squash 1a10a73 β†’ release 0afbf5c): Cargo.lock pin 3.14β†’3.14.1, deps-only, no worker source change, clippy clean. Built the prod image via Cloud Build (noetl-worker-rust:0afbf5c) and rolled it onto prod noetl-worker-rust + noetl-worker-system-pool (server stays v3.39.5/.6, worker CPU req 250m/limit 2 kept) β€” rolling restart clean (pods Ready, 0 crashloop), off-server CQRS cutover stayed healthy (materializer sole-writer projected==acked, lag ~0, command lag 0, executions completing, system-pool WAL index rehydrated per #119). ai-meta repos/toolsβ†’c8656c1 + repos/workerβ†’0afbf5c. The code-opt + CPU-bump compound: more headroom AND less work per slot on the batch hot path.
#123 2026-06-22 Loop with a non-iterable in: silently wedged (commands=0, RUNNING forever) instead of failing loudly β€” FIXED: the off-server drive now decodes the error envelope and emits a terminal playbook.failed. evaluate_loop already returns CoreError::Validation("… did not evaluate to an iterable") and the in-process drive already turned that into a terminal playbook.failed β€” but under the prod-default off-server drive (NOETL_ORCHESTRATE_PLUGIN_DRIVE=true) the system/orchestrate wasm plug-in returns a structured {"error":…} envelope instead of an OrchestrationResult, which apply_worker_orchestration couldn't decode β†’ logged a WARN, recorded decode_error, returned Ok(0) β†’ no terminal event β†’ the run sat RUNNING forever (this observability gap produced the false-positive #122 during the #120 2Γ—2 repro). Fix: server apply_worker_orchestration decodes the drive ERROR envelope (decode_orchestrate_error) and emits a terminal playbook.failed (metric noetl_orchestrate_drive_total{stage="drive_error"}, structured execution_id), matching the in-process drive β€” a transient decode miss stays on the benign re-drive path; orchestrate-core prefixes the offending step name onto the existing evaluate_loop error (prefix_loop_step_error). An empty iterable ([]/{}) still short-circuits to next β€” only a non-iterable errors. noetl-server v3.39.6 (server#258, squash 275b914 β†’ release 7f109a9); 600 server + 135 orchestrate-core tests + clippy clean; kind-validated prod-exact (PLUGIN_DRIVE=true+PUBLISH_ONLY=true+STATE_BUILDER=offserver): absent workload.batch_slots β†’ FAILED with loop step 'process': Loop expression '{{ workload.batch_slots }}' did not evaluate to an iterable (got null), a valid [1,2,3] loop still COMPLETED 3-way, #120/#124 unaffected, 0 pod restarts. Code-only fix (worker stays v5.40.3) β€” ships to prod on a future server rollout; PROD stays healthy on v3.39.5 + the live off-server cutover (NOT redeployed this session). #127 stays OPEN (perf follow-up, separate).
#121 2026-06-22 Off-server WAL-chain-incomplete loop on system/ executions β€” FULLY FIXED in two halves (v3.39.4 #256 + v3.39.5 #257). Second half (server#257, squash 54ac277 β†’ release v3.39.5 c421273): #256 alone did not hold β€” a live-prod re-cutover on v3.39.4 still wedged system/scheduled_cleanup because the system-pool worker drives system execs off-server regardless of the server-side gate (STATE_BUILDER=offserver on the worker β†’ INSERT-not-publish chain leaves a NULL-prev orphan). #257 gates both off-server-drive decision sites in trigger_orchestrator_inner on should_publish(catalog_id) (new pure is_system_path helper + unit test) so system/* execs fall through to server-built run_state; regular publishable execs keep the off-server path (preserves the #256 win). 612 tests + clippy; kind full-gate system/scheduled_cleanup BEFORE 9Γ— WAL chain incomplete β†’ AFTER COMPLETED 0 loops, regular test/simple_loop still COMPLETED off-server. Server-only (worker stays v5.40.3). First half (server#256, v3.39.4): two distinct defects. (1) The gate-off claim_command raw in-tx INSERT (and the handle_batch_events gate-off batch INSERT) bypassed the event_write::emit_events/ChainHeads chokepoint, so command.claimed got prev_event_id=NULL and the chain head was never advanced β€” the next event (command.started) linked back to command.issued, skipping the orphaned claim, so the off-server chain_walk_from hit a NULL-prev non-genesis head β†’ build_spine_to Incomplete. Fired for every system/ playbook because should_publish is false for system executions even under global PUBLISH_ONLY=true. Fixed by stamping prev_event_id via link_batch on both gate-off INSERT paths (same advance-then-write ordering emit_events uses). (2) The actual loop β€” the stateless off-server drive builds state from the noetl_events WAL, but system-execution events INSERT to noetl.event and never enter the WAL β†’ the worker's WAL build can never complete β†’ __offserver_retry__ re-drive loop. Fixed by gating the off-server WAL drive on should_publish(catalog_id); system/ executions drive server-built (reads noetl.event). noetl-server v3.39.4 (server#256, squash 28b17cb β†’ release 77aaa06); changes confined to src/handlers/events.rs + src/state.rs β†’ server-only rebuild, no worker bump. 598 tests + clippy clean; kind-validated prod-exact (PUBLISH_ONLY=true + STATE_BUILDER=offserver): system/scheduled_cleanup went orphan + 17Γ—+ WAL chain incomplete (wedged RUNNING 120s+) β†’ linked chain, 0 loop lines, COMPLETED in 6s; non-system off-server unaffected. Live prod repro: the full off-server cutover on prod (server v3.39.3 + off-server gate) WEDGED β€” drive applied froze while dispatched_offserver_stateless/offserver_retry climbed in lockstep to ~389, system pool logged the WAL-chain-incomplete no-op for multiple execs (incl. prod's own 327130580493803520), all prev_event_id=NULL; the armed revert (PUBLISH_ONLY=false+STATE_BUILDER=server) recovered cleanly β€” this is what reopened #121 and motivated the #257 second half above. PROD GKE default (STATE_BUILDER=server) + all defaults untouched. #123/#127 stay OPEN (separate).
#125 2026-06-21 task_sequence runner implemented only do: fail, silently ignoring do: jump/break/retry β€” FIXED: noetl-tools v3.14.0 honours all control-flow variants. The Rust task_sequence tool parsed the do: directive but only matched "fail", so do: jump/break/retry were silently ignored β€” each slot ran once and returned, leaving batches in progress with no retry/loop/jump path (pft_flow_test produced 16 done / rest pending, patient loss). Fix in tools#73 (noetl-tools v3.14.0, squash 62d0948 β†’ release 638c3c6): honour do: jump/to: (loop to named step), do: break (exit the sequence), do: retry (with attempts/backoff + infinite-jump guard). Worker adopts via worker#124 (v5.40.3). Together with #126, makes the full 10Γ—1000 batch pft_flow_test pass end-to-end on kind (zero patient loss, invariants clean). PROD + defaults untouched.
#126 2026-06-21 Rust http tool nested parsed response body under data.body instead of data β€” FIXED: noetl-tools v3.13.1 restores the Python-era output.data.data contract. The Rust http tool exposed the parsed response body under data.body, but playbooks written against the Python-era contract read output.data.data (or equivalently output.data for the body). pft_flow_test's save_batch step called jsonb_to_recordset on a non-array value β†’ error. Fix in tools#72 (noetl-tools v3.13.1, squash 86f0216 β†’ release 8dd0e1f): expose the parsed body under data again; keep body as a back-compat alias. PROD + defaults untouched.
#124 2026-06-21 Distributed task_sequence forward set:/sibling bindings rendered empty (data not propagated between sub-tasks) β€” FIXED: defer rendering of sub-task templates whose vars aren't resolvable at command-build time. Inside a multi-tool (task_sequence) step, a later sub-task's templates that reference a value a prior sub-task produces at runtime β€” a forward set:, a policy-rule set:, or a sibling result β€” were rendered to empty at command-build time, before the worker's per-sub-task binding ran. orchestrate-core/src/commands.rs::render_pipeline_config preserved set/args/spec/command verbatim but rendered every other sub-task field (url/params/method/…) against the step-entry context under UndefinedBehavior::Chainable, so runtime-only refs ({{ iter.* }}, sibling labels) silently collapsed to empty β€” concretely pft_flow_test's batch path: claim_batch writes iter.data_type via policy set:, then fetch_batch's url: …/api/v1/pft/batch/{{ iter.data_type }} pre-rendered to …/batch/ β†’ 404 β†’ 0 rows. Fix: new TemplateRenderer::render_value_deferring_unresolved renders only templates whose variable paths all resolve in the build-time context; any template referencing an unresolved path is preserved verbatim so the worker re-renders it against the per-sub-task running context (a superset). noetl-server v3.39.3 (server#255, 365d3be); cargo test 134/134 + clippy clean; kind-verified (fetch_batch now hits the real per-type URLs). PROD + defaults untouched. #121/#123/#125/#126 stay OPEN (separate).
#120 2026-06-21 Reduce barrier deadlocked (commands=0) on open/asymmetric loop joins β€” FIXED: a runtime liveness filter in the orchestrate-core reduce barrier. A back-edge U β†’ T whose forward return path from T is absent (T does NOT forward-reach U) was counted as a genuine fan-in parent of T, so dispatch of T deferred forever (U never runs on the taken path; pft_flow_test setup_facility_work stalled commands=0 for 4.5 min). The barrier now blocks on an upstream only if it is live on the current path (done/skipped, entered/in-flight, or reachable from an active not-yet-terminal step); build_incoming_arcs unchanged (an open back-edge is still a genuine static fan-in). Affects the in-server and off-server drives identically (shared orchestrate-core); closed loops + genuine reduces unaffected. noetl-server v3.39.2 (server#254, 28e8950); new unit test test_open_loop_back_edge_does_not_block_dispatch; 133/133 + clippy clean; kind-validated (post-fix 2Γ—2 off-server/gate matrix all COMPLETE, fanout_reduce/pagination/loop green). PROD + defaults untouched.
#119 2026-06-20 Off-server WAL state-builder drain stranded executions after a worker restart β€” FIXED: rebuild the in-memory index from the retained noetl_events WAL on every boot. The authoritative drain used a durable noetl_state_builder consumer whose cursor persists across restarts while the in-memory WalEventIndex rebuilds empty β†’ the cursor outran the fresh index β†’ build_spine_to(expected_head) permanently Incomplete β†’ off-server execs looped offserver_retry and never completed (this is what hid the #118 symptom). Fix (worker-only, inside NOETL_STATE_BUILDER=offserver; PROD runs the in-server drive so untouched): the drain defaults to an ephemeral DeliverPolicy::All consumer rebuilding the full index from the retained WAL on every boot (no persisted cursor to outrun; also correct for >1 worker pod); instant revert NOETL_STATE_BUILDER_DURABLE=1; proof = index rehydrated… log + new noetl_worker_state_builder_indexed_executions gauge; never reintroduces a noetl.event scan. noetl-worker v5.40.2 (worker#123, 48b0bde). Gate-ON kind: forced mid-flight pod delete --force β†’ new pod index rehydrated … indexed_executions=17 wal_events=200; single-replica 6/6 stress + multi-replica 21 execs COMPLETE, zero scans.
#118 2026-06-20 Single-replica off-server terminal-finalize chain fork (a duplicate playbook.completed orphaned the chain as a NULL-prev_event_id 2nd root) β€” FIXED: a bounded FinalizedGuard (exactly-one-terminal-per-execution) suppresses the duplicate at emit_events before the chain linker. A straggler drive on a materializer-lagged single replica re-drove off the lagged WAL (state not terminal yet) and emitted a 2nd terminal that linked to the now-evicted head β†’ 2 roots + a benign event-scan. The guard makes the first terminal win; gate-off byte-identical (a duplicate never occurs on the synchronous in-process drive); metric noetl_terminal_dedup_total{suppressed}; rig gains a HARD terminals==1 assertion. Absent under multi-replica execution-affinity (#116 serializes finalize to the owner). noetl-server v3.39.1 (server#253, c5f8cb2) + e2e (e2e#73, fe97d92). Gate-ON kind (unblocked by #119): single-replica 6/6 stress iterations / ~126 execs every chain roots=1(incl. terminal)/terminals=1/zero-scan; multi-replica 21 execs clean.
#117 2026-06-20 Off-server from_events spine ordered by event_id broke fan-in under a chain-order≠id-order inversion (high-concurrency fan-out reduce wedge) — FIXED: order the spine by the prev_event_id chain walk + walk from the real tip (expected_head). worker v5.40.1 (worker#122, baeae78) + e2e (#72, cdf1768). 2-replica affinity gate-ON stress 6/6 iterations / 108 execs COMPLETE; 15 real id-inversions all fired the fan-in reduce. The residual single-replica terminal-finalize chain-linking race is now FIXED as #118.
#113 2026-06-19 Worker-driven orchestrate drive stalled when the drive result / accumulated context crossed the inline budget β€” fixed at the source by #115 Phase 1 (references-in-state consume side). All 9 of 9 stalled core fixtures now reach playbook.completed gate-ON (PUBLISH_ONLY + off-server drive + materializer sole-writer), server v3.30.0 + worker v5.36.0: output_select / storage_tiers / lease_expiry / pipeline_heavy_payload / save_edge_cases / large_result_extraction / http_to_postgres_{direct,simple,bulk_python}. Max next-command context across the 9 = 412KB (save_edge_cases); 0 __orchestrate__ event rows on every run; materializer lag = 0. The #113 decode fix (server#241 v3.29.4) + #114 offload cap (server#242 v3.29.5) stay as safety nets; the growth that pushed past budget is gone (worker selective resolve keeps foreign bulk out of the render; refs_in_state-true keeps the drive state + command.issued bounded). Landed via worker#117 + server#243.
#114 2026-06-19 Off-server drive oversized command.issued (full upstream context embedded) β€” resolved by #115 Phase 1 (refs_in_state default true). With the worker selective consume side in place, refs_in_state defaults true so the next command's render_context carries {reference, extracted} (small) instead of the full upstream payload β€” command.issued no longer approaches NATS max_payload. The #114 offload safety cap (maybe_offload_command_context, 512KB) stays as belt-and-suspenders. Verified gate-ON: all 4 of this issue's fixtures COMPLETE; max next-command context 412KB across the full 9. Landed via server#243 + worker#117.
#112 2026-06-18 Worker /dev/shm SIGBUS β€” k8s default 64 MiB tmpfs vs the 256 MiB Arrow IPC cache budget. Every worker process (Rust noetl-worker + legacy Python worker) allocates an Arrow IPC shared-memory cache at init (NOETL_IPC_CACHE_BUDGET_BYTES, default 256 MB) backed by POSIX shm on /dev/shm; the k8s container-runtime default /dev/shm is a 64 MiB tmpfs, so under shm-heavy load the cache writes past 64 MiB, the store page-faults against the full tmpfs, and the worker dies with SIGBUS (exit 135) and crash-loops. Surfaced during #103 CQRS kind validation on the system pool; a transient live fix was reverted, leaving the committed manifests latent. Fix (ops#193) gives every worker deployment a memory-backed /dev/shm (emptyDir medium: Memory, sizeLimit: 320Mi > budget), pins NOETL_IPC_CACHE_BUDGET_BYTES=268435456 next to the sizeLimit so the two can't drift, and raises the memory limit to 768Mi (tmpfs is charged to the pod cgroup). Applied to all 7 worker manifests (system / shared-rust / subscription / subscription-runtime / Python-cpu + 2 prod variants). Kind-validated on the system pool: reproduced SIGBUS (exit 135) on the 64 MiB tmpfs β†’ after fix /dev/shm is 320 MiB and a full 256 MiB write completes (exit 0, peak 256M/320M) with the pod healthy (restarts=0, no OOM); cluster restored to baseline. ai-meta β†’ ops f4df4c1 + wiki worker deployment-specification.
#110 2026-06-18 Retired the in-server orchestrate shadow + the wasmtime server dependency. The separable server-slimming follow-up to #108 (closed): the in-server shadow was the slice-4 cutover-confidence harness (ran the system/orchestrate plug-in inside the server via an embedded wasmtime host, diffed 529/0 against the in-process drive). With the worker-driven drive default-on + proven, the live drive uses the worker's wasmtime host β€” never the server's β€” so the shadow was dead weight. server#234 removed orchestrate_shadow.rs, the orchestrate-shadow cargo feature + the optional wasmtime dep (the cranelift/wasmtime tree β€” ~1000 Cargo.lock lines β€” fell out; cargo tree -i wasmtime now matches nothing), the trigger_orchestrator_inner shadow hook, the main.rs boot loader, the NOETL_ORCHESTRATE_PLUGIN_SHADOW config field, and the noetl_orchestrate_shadow_total metric. Kept run_state + NOETL_ORCHESTRATE_PLUGIN_DRIVE (default true). refactor: = no version bump (stays v3.28.0). Kind smoke on a 4-page cursor loop, both drive modes: COMPLETED with __orchestrate__ rows in noetl.event = 0 (worker-driven: 10 drive cmds on the system pool, dispatched=applied=10, event_suppressed=30); shadow metric gone from /metrics. ai-meta β†’ server f3043c9 + wiki deployment-specification. See Umbrella-System-Pool-Design.
#108 2026-06-18 🎯 The orchestrator drive runs OFF-SERVER on the system pool, and it's now the DEFAULT. Step 2 of the dissolution (#107) β€” the server's brain moved onto the worker pool as the system/orchestrate WASM plug-in. Arc: slices 1–3 (off-server drive, server#229) β†’ 4a (cursor+fan-out validated) β†’ 4b + follow-up a (the __orchestrate__ meta-command touches noetl.event zero times, server#230/#231) β†’ (b) system-pool isolation (server#232 + worker#114 + ops#191) β†’ (c) the deliberate default-flip (server#233, v3.28.0): NOETL_ORCHESTRATE_PLUGIN_DRIVE defaults true. Gated on a scale soak (kind): a 694-drive cursor+fan-out run COMPLETED with __orchestrate__ rows in noetl.event = 0 and all 694 drives claimed on the system pool (shared pool got only the real steps); the default-on path (no env var) reproduced the identical shape (361 drives, isolated, 0 burst); 15/15 regression fixtures green; revert (=false β†’ in-process drive) verified. ai-meta β†’ server 80cc0e6 + worker 437b0be. See Umbrella-System-Pool-Design.
#109 2026-06-17 Event-ABI round β€” orchestrator + evaluate moved into the wasm core (#108 final slices). Slice 3 (server#223) relocated orchestrator/evaluate to noetl-orchestrate-core; the whole drive (renderer, playbook, commands, evaluator, state, orchestrator switch) now compiles native + wasm32-unknown-unknown. evaluate reads the pure core::event::Event; db::Event converts at the trigger_orchestrator boundary. 122 core + 565 server tests green, 0 WASI imports, kind PFT 10Γ—1000 clean (0 errors, 0 restarts). Plug-in round (data-plane ABI + command_emit + scheduler + shadow-diff) tracked under #108.
#106 2026-06-17 EventEnvelope rejects timezone-less timestamps — blocked the CQRS materializer. The tailer publishes to_jsonb(noetl.event row) and created_at is timestamp WITHOUT time zone, so the timestamp had no offset; EventEnvelope.timestamp: Option<DateTime<Utc>> rejected it with the misleading premature end of input. Fixed (server#217) with a flexible deserialize_with (RFC3339 + tz-less→UTC). Validated live: materializer projects {projected:0, duplicates:20} = byte-identical. The #103 shadow gate is green.
#105 2026-06-17 Plug-in compilation & hot-reload: WASM-compiled system playbooks + managed library. The WASM plug-in runtime is complete + fully live-proven on kind: dispatch (tool_kind: "wasm" → wasmtime host → digest from registry → run), capability flush (object_put → noetl.object_store), real data flow (input→args, worker#110), and hot-replace (worker#112) — republishing the same path@version swaps the running pool's behavior with 0 restarts. Landed across worker#93/#95/#97/#99/#101/#103/#105/#107/#108/#110/#112 + server#210/#212/#214 + tools#68 + docs#181. Remaining (executor: author sugar; compiled materialiser port) deferred to #104/#103. See Umbrella-WASM-Plugin-Compilation.
#100 2026-06-15 Cursor/claim loop mode: loop.spec.mode: cursor in the Rust orchestrator. noetl-server v3.8.0 (server#196) cursor loop engine + output namespace; noetl-tools v3.10.1 (tools#66) postgres -- comment splitter fix; worker#88 dep bump. test_pft_flow_v2 all_passed:true, 5/5 per data type on kind against the throttling/error-injecting paginated-api. See Umbrella-Cursor-Loop-Mode.
#99 2026-06-14 Transfer tool: Snowflake↔Postgres both directions, with credential-alias resolution. Both transfer arms implemented in noetl-tools v3.10.0 (tools#65); worker v5.22.0 (worker#87) pre-resolves source.auth/target.auth aliases; SFβ†’PG coerces string cells via $n::text::<udt> + reformats Snowflake-epoch timestamps to RFC3339; PGβ†’SF generates SQL-escaped INSERTs. e2e fixture migrated (e2e#58). Kind-validated bidirectionally against the live sf_test account: every step COMPLETED, real types preserved.
#90 2026-06-12 Subscription / listener tool (RFC) β€” all 7 phases shipped + live-proven. Bounded-drain tool (Mode A) β†’ kind: Subscription continuous runtime (Mode B) + header-directive engine β†’ gateway push-ingress (Mode C) + auth-gated directive trust β†’ store-and-forward spool + circuit breaker β†’ out-of-cluster Cloud Run + gcs spool β†’ CLI local noetl subscribe β†’ Phase 7 scale hardening. Final phase: server v3.5.0 (server#189) POST /api/execute/batch (Nβ†’N, partial-failure contained) + opt-in exactly-once dedup window (noetl.subscription_dedup, bounded-by-age, race-safe, default off); worker v5.19.0 (worker#79) batch dispatch + dedup opt-in + per-subscription rate limits (token-bucket fetch-side backpressure β†’ source keeps backlog, no loss); ops (ops#176) + e2e (e2e#48); no tools change. Live on kind: batch 12β†’12 COMPLETED on the subscription pool + per-message traceparent; dedup duplicateβ†’1 execution + subscription.message.deduplicated; rate-limit engaged + 10/10 β†’ executions (no loss). Refinement follow-ups tracked: #91–#94 + tools#57. ai-meta β†’ server 7b217d8 + worker 7531f4a + ops 6db69b9 + e2e 203593b.
#89 2026-06-11 JSON null re-injected via {{ step }} serialized as JS undefined — fixed in the server template renderer — the cursor pagination fixture's terminal page returns next_cursor: null; re-injecting the whole {{ fetch_page }} envelope into the next step's input rendered that field as the bare token undefined, producing invalid JSON. render_to_value then failed serde_json::from_str and returned the entire envelope as a raw string, so the consuming Python step got response as a str and crashed ('str' object has no attribute 'get'). Root cause was the server, not the worker (the issue's hypothesis): src/template/jinja.rs::json_value_to_minijinja maps JSON null→Value::UNDEFINED and minijinja's map repr emits undefined; the server's renderer was a divergent copy missing the | tojson retry the noetl-tools engine already had. Fix (server#177, v3.0.6) adds that retry — a lone {{ expr }} rendering container-shaped-but-invalid JSON re-renders with | tojson; minijinja_to_json maps undefined/none → JSON null. 5 new regression tests; 619 lib + 8 parity green. Kind-validated: cursor walks all 4 pages, terminal next_cursor: null handled, validate_results collects 35 (first_id=1, last_id=35, success) — matching offset. ai-meta → server 8e17fbe.
#88 2026-06-10 e2e offset/cursor pagination fixtures read response.body.* not response.data.* β€” the Rust http tool nests the parsed JSON payload under body ({{ fetch_page }} β†’ {body, headers, status_code}), so the fixtures' response.get('data', {}) resolved to {}, has_more/next_cursor defaulted falsy, and the loop exited after page 1 even with the post-#85 loop machinery correct. Fixed both check_pagination steps to response.get('body', {}) via e2e#40. Kind-validated against the live paginated-api test-server (Rust server/worker :dev): offset walks 0β†’10β†’20β†’30, has_more T/T/T/F, users 10/10/10/5, validate_results success 35 (first_id=1, last_id=35), playbook.completed COMPLETED. cursor path-fix correct + walks all 4 pages (Mg==β†’Mw==β†’NA==β†’null, 35 events fetched) but final collection blocked by a distinct worker bug β†’ filed #89 (terminal next_cursor: null serialized as JS undefined). Other pagination fixtures (retry/max_iterations/pipeline*/loop_with_pagination) share the same envelope-key assumption over /api/v1/assessments|flaky ({data, paging}) β€” flagged, left for follow-up. ai-meta β†’ e2e 72a7525.
#85 2026-06-10 Workflow-arc loops now advance across iterations + terminate cleanly β€” built on the dispatch-guard re-entry layer, two coupled orchestrator fixes via server#176 (v3.0.5). (1) Durable event-sourced loop-ctx propagation: step-level set: ctx.* loop variables were recomputed per pass and reverted to the workload default (loop thrashed 0,0,1,0,1,2,…); root cause was start's initializer set re-firing every pass in random HashMap order against check_pagination's advancing set. Fix persists each completion's rendered set: as a ctx.updated event (latest-wins fold + build_context overlay), emitted once per completion keyed by the stable completion event_id (not Utc::now()-fallback completed_at). (2) Loop-exit hang: the exit branch was marked step.skipped on a loop-body-completion pass (recency-based branch-point detector missed it), turning it terminal so the exit dispatch was suppressed; fixed with a structural loop-branch-point test. 614 lib tests (6 new; 2 verified to fail without their guard); clippy-clean. Kind-validated: counter loop advances 0β†’1β†’2β†’3 + terminates; real-http offset pagination advances 4 pages collecting 35. (Separate finding, filed follow-up: the e2e offset/cursor fixtures read response.data.users but the Rust http tool nests it at response.body.users.) ai-meta β†’ server e519fdc.
#87 2026-06-10 Multi-tool step: a later sub-tool can now reference an earlier sibling's output β€” in a tool: [list] step, each sub-tool's result was stored for the aggregated output but never injected into the running context, so {{ <label>.<field> }} rendered empty (masked in quoted positions; a syntax error at or near "," in unquoted numeric SQL positions, e.g. save_edge_cases test_large_payload). Fixed via tools#48 (v3.1.1): inject each sub-tool's result under its label (with a synthetic .data self-ref) so later siblings resolve it. Worker adopts 3.1.1 via worker#69. Kind-validated: save_edge_cases test_large_payload β†’ record_count = 100 (no syntax error), save_delegation_test clean. ai-meta β†’ tools 76f942a + worker b97f642.
#83 2026-06-10 Orchestrator fan-in barrier deadlocked workflow loops β€” build_incoming_arcs counted a loop back-edge (check_pagination β†’ fetch_page) as an upstream so the barrier deferred the loop head forever. Fixed via server#175 (v3.0.4): exclude back-edges via a new forward_reachable helper (genuine fan-in unaffected). Kind-validated; fanout_reduce green. ai-meta β†’ server 480ba72.
#84 2026-06-10 Orchestrator never populated event.name β†’ loop.done arc gates always skipped β€” when: {{ event.name == "loop.done" }} (10+ fixtures) never matched, so in-step loop: steps hung after completion. Fixed via server#175 (v3.0.4): inject event.name = "loop.done" into a completed loop step's next-arc context. Kind-validated (test_pagination_basic completes). ai-meta β†’ server 480ba72.
#86 2026-06-10 e2e fixtures: duckdb tool field is command/query, not commands β€” commands: (plural) failed pre-dispatch (missing field 'query' / malformed tool config). Renamed across 4 storage/gcs fixtures via e2e#39; save_all_storage_types green. ai-meta β†’ e2e b0a5c85.
#78 2026-06-10 noetl-worker: pre-dispatch errors now emit terminal call.error instead of hanging β€” credential-alias resolution + tool-config deserialization failures used to ?-propagate out of execute_with_server_url; the dispatch loop only logged them, so the execution sat at command.started forever. Fixed via worker#68 (v5.15.1): typed CredentialResolutionError (terminal AliasNotFound/Invalid vs retryable Transient) + CredentialHttpError carrying the HTTP status so classify_fetch_error decides retryability by code (terminal: 404/400/401/403/500; retryable: 408/429/502/503/504 + transport), and handle_predispatch_failure emits call.error + command.failed. Diagnosis correction: the live pg_noetl_k8s repro is an HTTP 500 "Decryption failed: aead::Error", not a 404 (the worker has no /api/keychain/ call). Kind-val GREEN: test/postgres now β†’ call.error/command.failed/playbook.failed (no hang); hello_world still completes. ai-meta β†’ worker 99e2c66.
#80 2026-06-10 container_callback chain green end to end — fixing the watcher's missing curl (retired bitnami/kubectl → alpine/k8s:1.30.3, ops#168) surfaced two more layered bugs. Server: container-callback call.done insert targeted a non-existent attempt column → HTTP 500; fixed to the deployed noetl.event schema (server#173, v3.0.3). OOM path: the watcher only read Job-level conditions so failed_oom could never fire — added pod-level OOMKilled→failed_oom / ImagePullBackOff→failed_image_pull classification + RFC3339 completed_at (bare now was returning HTTP 422) (ops#168); and the e2e fixture's calloc-lazy bytes(40MiB) never dirtied pages so it never OOM'd (e2e#38). kind_validate_container_callback.sh both probes GREEN — happy_path → succeeded, oom → failed_oom. ai-meta → ops cacc513 + server 5d2cf58 + e2e 6aaf06e.
#79 2026-06-10 e2e kind-val runner scripts updated to current noetl CLI surface β€” both scripts/kind_validate_*.sh aborted on unrecognized subcommand 'playbook'; the validation logic + event taxonomy were intact, only the invocation layer drifted. Fixed via e2e#37: register playbook / exec <catalog-path> --runtime distributed --json / status --json / event log via noetl query (.result, order by event_id); fail-fast CLI-surface guard. fanout_reduce PASS start-to-finish on kind (server-rust v3.0.1); container_callback drives cleanly and stops at the watcher curl gap (#80). ai-meta β†’ e2e a3594b3; wiki Kind-Val Runners.
#82 2026-06-10 GUI: credential View/Edit recovered for pre-wallet (legacy-encrypted) records β€” Secrets Wallet (#61) made credential storage forward-only, so pre-wallet records 500 on decrypt and the GUI View/Edit flow dead-ended on a generic toast. Fixed via gui#36: View explains the cause + points to Edit; Edit reopens with the list-row metadata + a warning banner so re-saving re-seals the record under the current wallet. ai-meta β†’ gui 8cacc9e (v1.11.1).
#81 2026-06-10 Container tool unusable: ToolSpec.command (String) vs ContainerConfig.command (Vec) type contradiction β€” landed via server#172 (v3.0.2). ToolSpec.command Option<String> β†’ Option<serde_json::Value> so the container tool's array command decodes server-side + passes through to the worker's Vec<String> (scalar stays a JSON string for shell/db tools); ToolCall::from_spec forwards verbatim. 2 regression tests; clippy clean. Kind-val GREEN: server accepts command: ["/bin/sh","-c"], worker creates the K8s Job, Job reaches Complete 1/1. Chain counter-bump validation stays gated on #79/#43.
#77 2026-06-09 Explicit input:/set: forward-only data binding β€” BREAKING v3.0.0 across noetl-tools + noetl-server. All 5 PRs merged: tools#45 (v3.0.0) + server#169 (v3.0.0) + e2e#35 (13 fixtures migrated) + cli#57 (v4.10.0, executor 0.5.0) + worker#66 (dep bump). Kind-val GREEN.
#76 2026-06-08 Sequential-mode iterator dispatch β€” LoopMode enum (Sequential default / Parallel), StepInfo.iterations_dispatched guard. Landed via noetl/server#166 (v2.62.0). First Claude-direct Rust PR under agents/rules/handoff-routing.md. Kind-val GREEN: test/loop COMPLETED 5/5 + iterator_save_test COMPLETED 4 steps.
#70 2026-06-08 noetl-server missing PUT /api/result/<execution_id> endpoint β€” landed via noetl/server#160 (v2.58.0). Durable result-store endpoints (PUT + GET /api/result/<eid>/<step>/<ref>) ported to the Rust server. Kind-val GREEN: output_select_test reached playbook.completed with test_result: "PASSED".
#69 2026-06-08 noetl-worker: over-budget call.done returned reference-only envelope, missing inline _ref for downstream {{ step._ref }} templates. Landed via noetl/worker#63 (v5.15.0): build_call_done_result's durable-success branch now embeds context: { data: { _ref: <noetl://...> } } alongside the existing reference block. Kind re-val pending noetl/ai-meta#70 (server-side PUT /api/result/<eid> endpoint missing β€” falls back to degraded shm-only path where there's no noetl:// URI to embed).
#68 2026-06-08 noetl-tools: ArtifactTool config required input but server pipeline emitted args (the post-#56 normalized name) β€” landed via noetl/tools#40 (v2.23.1) + worker dep bump via noetl/worker#62. One-line #[serde(alias = "args")] on ArtifactConfig.input accepts both shapes. Re-val surfaced a downstream _ref/output_select gap β†’ filed as noetl/ai-meta#69.
#67 2026-06-08 Rust orchestrator hangs on mode: exclusive routing β€” untaken sibling never emits step.skipped, R4 fan-in barrier deadlocks. Landed via noetl/server#159 (v2.57.2). Three-part fix: (1) evaluator::evaluate_next_transitions surfaces unmatched siblings as not_matched_with_target; (2) orchestrator::process_in_progress two-pass emit-skipped-then-dispatch (HashMap-order-independent); (3) +2 unit tests (lib 568/0/0). Kind-val GREEN β€” comprehensive_test.yaml reached playbook.completed in ~4s (was hanging forever pre-fix).
#66 2026-06-07 Rust orchestrator: cross-step {{ step.data }} template resolves to None β€” landed via noetl/server#158 (v2.57.1). WorkflowState::build_context injects a self-referencing .data key on extracted user_data, guarded so the task_sequence flatten path's existing .data stays intact. 2 new unit tests + 566/0/0 lib. Surfaced by #65 kind-val; concrete repro kind execution 322087210360770560.
#65 2026-06-07 noetl-tools: external python script loaders (file/gcs/http source types) + legacy main() function convention β€” landed via tools#38 (v2.22.0) + tools#39 (v2.23.0); kind-val GREEN on the live worker (execution 322087210360770560 reached playbook.completed in ~6s; loaded main(name, count) returned the expected payload). noetl/worker#61 OPEN+mergeable to bump the worker pin. Surfaced finding: noetl/ai-meta#66.
#43 2026-06-07 R-3 Phase C-2: container tool kind β€” design callback pattern for K8s Job dispatch. All four Rust rounds shipped (1 k8s-watcher ops@8892043 / 2 callback endpoint server v2.48.0 / 3 Tool::Container tools v2.21.0 / 5 kind-val rig e2e@17de21d). Round 4 (Python parity) parked per Rust-only direction. Worker-side pending_callback adoption is a coordinated follow-up.
#64 2026-06-07 noetl-tools: artifact tool kind missing from Rust registry β€” landed via tools#35 v2.20.0 (thin ArtifactTool adapter translating the Python-era YAML shape into a ResultFetchTool call)
#61 2026-06-07 Secrets Wallet (Rust): envelope encryption + KMS/secret-provider plugins + sealed worker delivery + distributed multi-region resolution β€” all named phases + 6d.X cloud dynamic providers shipped; umbrella feature-complete
#54 2026-06-06 Phase F R5 β€” regression + e2e validation under sharded topology β€” closed at the umbrella level (Tier 1 + Tier 3 + Tier 4 e2e all GREEN on the Rust-only stack; subsequent regression findings filed as their own ai-task issues β€” #62/#63/#64/#65/#66 all now also closed)
#62 2026-06-05 noetl-server: /api/executions list query candidate-first rewrite + status-drift fix (server#99 v2.28.1) β€” 6.5 s β†’ 0.015 s (~430Γ—)
#63 2026-06-05 noetl-tools: python tool accepts nested script.source.code (inline) β€” tools#33 v2.19.3 + worker#54 adoption, test_script_loading kind-validated; external loaders split to #65
#60 2026-06-04 Rust orchestrator template context doesn't expose step data for next.arcs / step.when
#59 2026-06-04 Rust orchestrator doesn't resolve tool.kind:workbook references to inline actions
#58 2026-06-04 Rust orchestrator doesn't emit playbook.failed on command.failed β€” executions stall
#57 2026-06-04 Rust server rejects flat (name-as-field) pipeline shape in v10 playbook YAML
#56 2026-06-04 Canonical v10 playbook workload + input alias unrecognized on Rust stack
#55 2026-06-04 Rust server EventEmitRequest.execution_id wire-type drift blocks worker traffic
#53 2026-06-04 Rust worker β†’ Rust server e2e compatibility (Phase D R3b/R3c terminal completion gap)
#52 2026-06-03 noetl-tools: add js_consume operation to nats tool kind
#51 2026-06-03 Fix system/outbox_publisher.yaml auth block to use AuthResolver pattern
#50 2026-06-03 Phase 2.a β€” system/outbox_publisher playbook + routing + auth wiring

Ecosystem map

See the Repo Map page for the full submodule inventory. Quick view of the production repos and their current versions (2026-07-30 β€” prod runs on GKE Autopilot noetl-prod-autopilot, us-central1, project shastaratech-noetl-prod, migrated off noetl-demo-19700101 on 2026-07-26; since 2026-07-27 the command bus is EHDB, not NATS β€” NATS remains installed only as the rollback path; since 2026-07-30 the bus is also ~2.4Γ— faster than NATS on server v3.58.3 / worker v5.81.3):

Repo Role Version Recent
noetl/server Rust control plane v3.123.2 in prod β€” verified 2026-10-07 from the pod: noetl_server_build_info{version="3.123.2"}, digest 49eb47fc, 1/1, restarts 0, env 67. ⚠ This cell said "v3.123.0 in prod, digest 091ff47f" until the 2026-10-07 sweep β€” a stale version assertion of exactly the class Rule 0a exists to catch. Read the version from the pod digest, never from this table. 🟒 Scoped tier repair endpoint (2026-09-04) β€” POST /api/ehdb/repair/executions/{id} (server#399) re-mirrors only the event IDs the cross-store comparator reports absent. Inert: no caller, no sampler, not run against prod. Both comparisons under ParityRecording::Inspect so a repair does not write the counter its own alert reads (#264). Prior: 🟒 T5-readiness deploy (2026-07-31) β€” EHDB publish retry widened to a 10 s wall-clock deadline so it spans a writer pod restart (server#291); verified live at 60/60 with 0 publish errors across a mid-burst writer restart. Prior: ⚑ v3.58.3 β€” the EHDB command bus is now ~2.4Γ— faster than NATS in PROD (2026-07-30) β€” server#289 drops the publish mutex held across the writer RTT (pipelined publish) and server#290 retries a publish across a writer restart. Prod: dispatch p50 138.5 / p99 181.0 ms vs the 338 ms NATS baseline (#205 CLOSED), writer-restart 38/38 with 0 loss (#208 CLOSED). Held: server#291 widens that retry to a 10 s deadline. Prior: πŸš€ L1 T4 β€” server publishes commands to the EHDB feed in PROD (2026-07-27) β€” NOETL_COMMAND_BUS=ehdb live on shastaratech prod; server NATS_pub=0, 80/80 0-loss 0-dup across the flip + soak. v3.58.1 pins the #203 writer-assigned-ordering-key fix (ehdb#300). Prior: πŸ” #169 Auth0 JWT signature verification β€” SHADOW canary LIVE on prod (2026-07-04, server v3.50.0β†’v3.52.0, single replica) β€” handlers::auth_verify fetches the tenant JWKS + verifies the RS256 signature + iss/exp/nbf (aud when set) behind NOETL_AUTH_VERIFY_SIGNATURE (tri-state; shadow = verify + meter + always allow, cannot lock anyone out). Awaiting one real-login outcome=success metric, then audience + enforce (gated; single replica = all-or-nothing). Handover Handover-169-JWT (server#276, #169). Prior: 🧟 #171 orphaned-command guardrail LIVE on prod (2026-07-02) β€” server-side flag-gated sweep terminates dead-worker-orphaned RUNNING execs append-only (playbook.failed) once the owner is absent/stale in noetl.runtime; released ghcrβ†’AR server-rust:v3.50.0, flag NOETL_ORPHAN_SWEEP_ENABLED=true (server#271); prod-proved a killed-worker orphan (mid-call_duffel_offers, no NATS redelivery) swept to FAILED at grace(180s)+one tick, {candidate=1,terminated=1} idempotent, 0 collateral, 0 restarts; rollback = flag off β†’ v3.49.0. Prior: πŸ” #168 synchronous auth fast-path LIVE on prod (v3.49.0, 2026-07-01) β€” session-validate + login run in-process off the slow drive (NOETL_AUTH_SYNC / NOETL_AUTHZ_SYNC). Prior: ⚑ #156 off-server drive tail-attach DEPLOYED on prod (2026-06-29) β€” the server attaches the just-published per-execution event tail (ChainTails ring β†’ tail_events) to the __offserver_build__ dispatch so worker state rehydration is O(execution) not O(global event-log), removing the drain-lag 8s reconcile cliff; flag-gated NOETL_OFFSERVER_ATTACH_TAIL default off (server#268, v3.48.0 ddca5c4; rolled flag-off behavior-neutral then flipped on, 0 restarts, turns identical correct output, 1-step rollback). Prior: πŸš€ #104 result_store dual-write RETIRED on prod (2026-06-27) β€” NOETL_RESULT_STORE_DUAL_WRITE=false flips PUT /api/result/{eid} to mint_ref_only() (byte-identical ref, skips the noetl.result_store INSERT; over-budget results served from the #104 tier by URN). Deployed v3.47.0 @sha256:140066d9… behavior-neutral, then flipped on prod: result_store writes stopped (dual_write_skipped_total climbs, put_total/dual_write_total flat), over-budget execs COMPLETE, sole-writer balanced 142==142==142 / lag-0 / dup-0, 0 restarts; existing result_store data frozen, not deleted; revert one step = flagβ†’true (server#267, v3.47.0 bd831e6; ai-meta pointer this change set). Prior: πŸ†• G3 registry MERGED + RELEASED (review-only) β€” versioned model/dataset/eval registry on the #104 object tier, flag-gated NOETL_REGISTRY_ENABLED default-off (server#266, v3.46.0 1d86464; ai-meta pointer 2e79af6). Prior: πŸš€ #104 β€” WI/ADC GCS auth MERGED + ROLLED TO PROD as KSA noetl-server-rust (3-mode none/static/adc auth matrix via gcp_auth; auth=autoβ†’adc on real GCS, mints+auto-refreshes a short-lived WI/ADC OAuth token for the PAP-enforced bucket) (server#265, v3.45.0 fad5d8a; prod server @sha256:d3cbf1ad… runs as the WI-bound KSA with result-tier GCS ENV applied β€” all tier-enable flags OFF β†’ tier inert, off-server cutover stayed sole-writer/lag-0/never-scan, 0 restarts; ready for staged Bβ†’Cβ†’D enablement). Prior: πŸ“¦ #104 Phase F MERGED β€” result-tier GC sweeper (NOETL_RESULT_TIER_GC, default off): conservative dry-run-first sweep reclaiming only provably-dead tier objects, never deletes a live-referenced one (server#264, v3.44.0 341b614; default-off byte-identical, inert in prod). Prior: πŸ“¦ #104 Phase D MERGED β€” mint-authoritative flag + result_store dual-write counter (NOETL_RESULT_MINT_AUTHORITATIVE, default off; the result_store PUT records each write on noetl_result_store_dual_write_total as the reversible dual-write fallback leg) (server#263, v3.43.0 6f6b9ef; no Cargo.toml change, resolves noetl-locator 0.1.1 from the registry; default-off byte-identical, inert in prod). Prior: πŸ“¦ #104 Phase C β€” GCS object backend + cell-endpoint registry + GET /api/internal/cells (the server side of the resolve-by-URN read path; default-off, inert in prod until a future rollout) (server#262, v3.42.0 c2d5ca9; no Cargo.toml change, resolves noetl-locator 0.1.1 from the registry). Prior: πŸ“¦ #104 Phase B β€” ensure the sibling noetl_result_materializer durable consumer at stream-birth so the worker's result-materializer loop drains on its own ack cursor (object-store latency never back-pressures the noetl.event audit fold); no new deps, control plane stays slim (server#261, v3.41.0 4a6659e; flag-gated on the worker side, inert in prod). Prior: πŸ“¦ #104 Phase A β€” accept the canonical result URI behind NOETL_RESULT_URI_ACCEPT (default off), parsed via the slim dependency-free noetl-locator 0.1.0 (heavy graph stays off the control plane) (server#260, v3.40.0 c89d078): dep repointed gitβ†’0.1.0 pre-merge, heavy crates (duckdb/kube/arrow/tonic/rhai/gcp_auth) confirmed absent, 623 tests green; inert in prod until a future server rollout enables the flag. Prior: βœ… #123 β€” surface a non-iterable loop in: as a terminal playbook.failed (off-server drive decodes the {"error":…} envelope via decode_orchestrate_error; orchestrate-core prefixes the offending step name onto the existing evaluate_loop error) (server#258, v3.39.6 7f109a9, squash 275b914): a non-iterable loop.in: used to silently wedge (commands=0, RUNNING forever) under the off-server drive because apply_worker_orchestration couldn't decode the wasm plug-in's error envelope (logged WARN, recorded decode_error, returned Ok(0) β€” no terminal event); now it emits a terminal playbook.failed (metric noetl_orchestrate_drive_total{stage="drive_error"}, structured execution_id), matching the in-process drive. An empty iterable ([]/{}) still short-circuits to next. Code-only (worker stays v5.40.3); 600 server + 135 orchestrate-core tests + clippy clean; kind-validated prod-exact (absent workload.batch_slots β†’ FAILED with a clear message, valid [1,2,3] still COMPLETED, #120/#124 unaffected, 0 restarts); ships to prod on a future rollout. Prior: βœ… #121 SECOND-HALF β€” gate BOTH off-server-drive sites in trigger_orchestrator_inner on should_publish so system/* execs drive server-built run_state (they INSERT to noetl.event, never enter the noetl_events WAL); regular execs keep the off-server path (server#257, v3.39.5 c421273, squash 54ac277); pure is_system_path helper + unit test; server-only (worker stays v5.40.3); 612 tests + clippy; kind full-gate system/scheduled_cleanup BEFORE 9Γ— WAL chain incomplete β†’ AFTER COMPLETED 0 loops, regular loop still off-server (#256 was only the first half; #121 reopened after the v3.39.4 prod re-cutover still wedged). Prior: βœ… #121 first-half β€” link the gate-off command.claimed through ChainHeads (server#256, v3.39.4 77aaa06, squash 28b17cb). Prior: βœ… #124 β€” distributed task_sequence forward set:/sibling bindings no longer render empty (defer sub-task templates with unresolved vars to the worker re-render) (server#255, v3.39.2 28e8950): a runtime liveness filter in the orchestrate-core reduce barrier β€” an open/asymmetric loop back-edge predecessor that never runs on the taken path is no longer counted as a pending fan-in dependency (build_incoming_arcs unchanged); affects in-server + off-server identically; test test_open_loop_back_edge_does_not_block_dispatch, 133/133 + clippy, kind-validated. Prior: βœ… #116 program-scale step 2 β€” execution-affinity single-owner WRITE ORDERING (multi-replica gate-ON validated) (server#252, v3.39.0 5e00d0a): closes the chain-fork race step 1 left open (the command.issued prev-read + head CAS-advance are two non-atomic steps β†’ concurrent cross-replica emits forked the chain). Affinity routes every trigger for an execution (POST /api/events, which fires the drive) to the single replica that sharding::ShardConfig::owns(execution_id) owns (stable XxHash64); non-owner forwards a reverse-proxy POST (one-hop loop guard, degrade-to-local). On the owner the single-process drive lock + in-memory ChainHeads make the readβ†’advance atomic, no distributed lock; KV is the genesis/handoff vehicle (owner resolves LOCAL β†’ kv_remote_hitβ†’0). New src/affinity.rs; NOETL_EXECUTION_AFFINITY/NOETL_PEER_URL_TEMPLATE/NOETL_SHARD_INDEX_FROM_HOSTNAME (all default off, prod unchanged); metric noetl_execution_affinity_total{outcome} (forwarded_ok = proof). Multi-replica gate-ON kind (2-replica StatefulSet, offserver+audit_only+nats_kv+affinity+publish_only): rig PASS β€” chains roots=1/dangling=0/walk==total (NO fork), forwarded_ok +9, never-scan + sole-writer across replicas, executions COMPLETE; single-replica unchanged; 595 tests + clippy green. Follow-up #117: off-server from_events spine ordered by event_id wedges fan-in under a chain-orderβ‰ id-order inversion (high-concurrency fanout). Prior: βœ… #115 program-scale step 1 β€” multi-replica coherence DATA LAYER (NATS-KV-backed ChainHeads + ExecDescriptor); execution-affinity STAGED (server#251, v3.38.0 8f39a79): NOETL_REPLICA_COHERENCE=nats_kv (default local, prod unchanged) backs the off-server drive's watermark + descriptor with JetStream KV buckets β€” head advance = CAS (one chain under concurrent emits), descriptor = CAS merge; in-process maps = write-through cache / degraded fallback. src/coherence.rs; ChainHeads/ExecDescriptors async; metric noetl_replica_coherence_total{structure,op,outcome} (proof kv_remote_hit). Kind: single-replica nats_kv bit-for-bit parity with local (all topologies COMPLETE, clean chains, scans +0); 2-replica proved cross-replica resolves work. Necessary-not-sufficient: 2+ replicas still fork the chain (non-atomic issuing_event-read vs head-advance) β†’ execution-affinity STAGED as step 2 (substrate in src/sharding.rs). Prior: βœ… #115 Phase 5 β€” atomic-working-item context (tenet 6): the drive hands a worker only its minimal declared slice (server#250, v3.37.0 a96ade8): new orchestrate-core::input_binding (analyze/project_context over minijinja undeclared_variables, conservative bail-to-full) + CommandBuilder::with_atomic_item_context; NOETL_ATOMIC_ITEM_CONTEXT (default false, prod unchanged); metric noetl_atomic_item_context_total{outcome}. Builds on #77 (Explicit Input Binding, CLOSED). Gate-ON kind-validated: flag-ON consumer ctx narrowed to the one declared key, COMPLETED; flag-OFF full ctx (back-compat). Prior: βœ… #115 Phase 6 β€” retire the hot-path noetl.event read class; the table is AUDIT-ONLY (server#249, v3.36.0 b71ca1d): NOETL_EVENT_READ_PATH=event_scan|audit_only (default event_scan, prod unchanged). Under audit_only, the remaining lifecycle readers of noetl.event (the WHERE execution_id replay class outside the drive β€” get_catalog_id per-ingest, inherit_parent_trace, subscription dedup-audit + container-callback catalog/existence) serve from the in-memory execute-time ExecDescriptor; a cold descriptor (post-terminal straggler after eviction / restart) resolves catalog_id from noetl.command (the synchronous queue) β€” never a noetl.event scan. New proof metric noetl_event_hotpath_reads_total{site,outcome}. Gate-ON kind-validated: hot-path scan Ξ”0 (served_descriptor +96 + served_command +3), drive state_build_total Ξ”0 + event_scans Ξ”0 β‡’ ZERO noetl.event scans anywhere on the hot path, end-to-end; linear/loop/fan-out/output_select COMPLETE; sole-writer + lag-0; audit still works (direct SELECT + status + replay event_count=25); 585 tests + clippy green; default event_scan, prod unchanged. The RFC never-scan end state (tenet 3) is reached under the flag. Prior: βœ… #115 Phase 4 REMAINDER β€” stateless off-server drive edge (zero state rebuild + zero noetl.event reads) (server#248, v3.35.0 6e30fc3): removes the residual server-side chain-walk bookkeeping on the drive path. Under NOETL_STATE_BUILDER=offserver a warm execute-time ExecDescriptor (catalog_id + routing seeded at playbook_started; terminal stamped at the emit_events chokepoint) lets trigger_orchestrator_inner route the system/orchestrate command WITHOUT building WorkflowState β€” expected_head from the in-memory ChainHeads, trigger_event_id passed so the worker resolves the trigger type off its WAL, NO server-built state (__stateless__). apply_worker_orchestration sources catalog_id+routing from the descriptor (skips cold-rebuild) + evicts on terminal worker-built state. Cold descriptor (restart) falls through to the server-built path (re-seeds) β†’ chain_walk + event_scan stay fallbacks. Proof: noetl_orchestrate_drive_total{dispatched_offserver_stateless,applied_stateless} advance while noetl_state_build_total stays flat. Gate-ON kind-validated: state_build_total Ξ”0, event_scans Ξ”0, dispatched_offserver_stateless +3 / applied_stateless +3, linear(13)/loop(62)/fan-out(25)/output_select(31) COMPLETE, offserver==server parity, sole-writer 25==25, lag-0; 583 tests + clippy green; default server, prod unchanged. Completes #107 step 2 server-side. Prior: βœ… #115 Phase 4 DRIVE CUTOVER β€” mark the off-server drive command + carry expected_head (server#247, v3.34.0 f0922bd): under NOETL_STATE_BUILDER=offserver (default server, prod unchanged) trigger_orchestrator_inner marks the system/orchestrate command __offserver_build__ + carries execution_id + expected_head (the staleness watermark) so the worker self-sources the drive state from the WAL; the server-built state rides along as the worker's incomplete-chain fallback. Gate-ON parity rig PASS (offserver==server, fan-in exactly-once, server scans 0). 580 tests + clippy green; no prod default changed. Prior: βœ… #115 Phase 4 β€” NOETL_STATE_BUILDER=offserver|server flag scaffold (default server) (server#246, v3.33.0 3e6006d): the server-side flag for the off-server state-builder drive cutover. server (default, prod unchanged) builds WorkflowState in-process; offserver routes the drive to obtain state from the pool-side off-server builder (worker state_builder, reading the noetl_events WAL). Flag only β€” the offserver drive-cutover wiring is staged (pool-side builder landed in worker v5.37.0). 2 config tests; no prod default changed. Prior: βœ… #115 Phase 3 MERGED β€” chain-walk state builder (flagged, default-off) (server#245, v3.32.0 8338417): behind NOETL_STATE_BUILD_MODE=chain_walk the drive reconstructs WorkflowState by following the one-level prev_event_id chain headβ†’root (head from the in-memory ChainHeads watermark, then (execution_id,event_id) PK point-lookups β€” never a WHERE execution_id scan of noetl.event) and feeds the same from_events (orchestrate-core unchanged β†’ parity by construction). Falls back to event_scan (default) on cold-head / materializer-lag / non-genesis so correctness is never sacrificed; NOETL_STATE_BUILD_PARITY_CHECK shadow-builds both ways in one REPEATABLE READ snapshot and asserts equal. New metrics noetl_state_build_total{mode,outcome} + noetl_state_build_event_scans_total (no-scan proof, 0 under chain_walk) + noetl_state_build_chain_hops + noetl_state_build_parity_total{result}. Gate-ON kind-validated: parity 41/41 MATCH, event_scans_total=0 / 1064 PK hops / 0 fallbacks, all topologies COMPLETE, sole-writer + lag-0, 577 lib tests + clippy green. Self-merged; prod default unchanged. Phase 4 (off-server builder + WAL cache) builds on this. Prior: βœ… #115 Phase 2 MERGED β€” one-level prev_event_id event chain (server#244, v3.31.0): each noetl.event carries prev_event_id (the immediately-previous event in causal order) + each noetl.command the issuing-event link, stamped at the emit chokepoint emit_events from a per-execution chain-head watermark (ChainHeads) β€” covering drive events + command.issued + worker-lifecycle on both gate paths, the materializer persisting it. Additive (nothing reads the link yet β€” that is Phase 3, in progress). Chain-correctness kind-proven walkable/1-root/no-gap/no-scan across 6 gate-ON topologies; 573 lib tests + clippy green. ai-meta pointer afdb365. Prior: βœ… #115 Phase 1 β€” surface _ref/_store on kept refs + refs_in_state default true (server#243, v3.30.0): hydrate_result_references keep_refs branch merges ref/store/uri onto the bounded extracted summary it surfaces as context.data (so {{ step._ref }} lazy-load + {{ step._ref is defined }}/{{ step._store }} predicates resolve off the summary without bulk), and refs_in_state now defaults true β€” references stay out of state + commands (drive state + command.issued bounded; the worker selective consume side landed in worker#117). Closed #113 + #114; kind gate-ON all 9 stalls COMPLETE. Prior: 🐞 #114 β€” offload oversized command context (server#242, v3.29.5): a command.issued context over NOETL_COMMAND_CONTEXT_MAX_BYTES (512KB) is offloaded to noetl.result_store with a {__context_ref__} marker; get_command/claim_command resolve it before the worker sees the command (metrics context_offloaded/context_ref_resolved) β€” so the published event stays under NATS max_payload and never wedges the publish-only gate. Kind gate-ON: rig PASS, every command.issued event <1MB, 6 of #113's 9 fixtures COMPLETE; chose ref-on-oversize over refs_in_state=true (the latter breaks bulk-consuming fixtures β€” consume side not impl). Remaining 3 fixtures + the cutover gated on the refs_in_state consume side (#101). Prior: 🐞 #113 β€” off-server drive: recover offloaded drive result + stop drive on cancel (server#241, v3.29.4): apply_worker_orchestration resolves+decodes an OFFLOADED __orchestrate__ drive result (over the 100KB inline budget β†’ durable reference.ref, no inline output_b64) instead of dropping it β†’ non-convergent re-loop (new noetl_orchestrate_drive_total{stage=ref_resolved}); cancel now stops the drive (match underscore playbook_cancelled + ExecutionState::is_terminal terminal guard evicts the orch-cache, no restart). Kind gate-ON proven (785KB result β†’ ref_resolvedβ†’COMPLETED, 0 decode WARNs; cancel froze a drive-loop instantly; sole-writer lag 0); rig e2e#63. 5/9 #113 fixtures COMPLETE; 4 hit a distinct oversized-command.issued stall β†’ #114. No prod default flipped. Prior: 🎯 #103 β€” CQRS write-path cutover COMPLETE, FLIP-READY (server#240, v3.29.3): the 2 ExecutionService cancel/finalize writers route through the emit_event chokepoint β€” the last synchronous server noetl.event writer under the gate is closed, so with NOETL_EVENT_INGEST_PUBLISH_ONLY=on the server writes zero event rows (materializer is the sole writer). Kind-proven both modes (gate-off byte-identical INSERT; gate-on PUBLISHED + materializer sole writer + terminal state + 0 loss/dup). All three flip blockers closed β†’ flipping the gate on is a staged operator decision. Default-off; no prod default changed. Prior: #104 β€” off-server-drive Γ— gate crash-recovery (v3.29.2, server#238): cold-cache apply rebuilds WorkflowState from the durable log. Prior: #108 (c) β€” worker-driven orchestrator drive now DEFAULT ON (server#233, v3.28.0): NOETL_ORCHESTRATE_PLUGIN_DRIVE defaults true after the scale soak proved zero noetl.event burst + full system-pool isolation; in-process drive kept as the =false revert. Prior: #101 β€” bounded-memory orchestrator + stall-proof reconcile (v3.9.0, server#197): projection-snapshot bounded rebuild (flat memory β€” 167KB snapshot at 200k events, was OOM at ~19k); throttled O(events) consistency COUNT off the hot path; background reconcile poller (force-advances every active execution every 8s β†’ no permanent deadlock under DB backpressure); results-by-reference resolution; GET /api/executions/{id} memory-bomb fix. Validated kind 10Γ—1000 (flat memory) + GKE db-g1-small/PgBouncer 10Γ—200 (poller broke a stall, 0 fails/restarts, Cloud SQL ~15 backends). Prior: #90 Phase 7 β€” POST /api/execute/batch + opt-in exactly-once dedup window (v3.5.0, server#189, closes server#188): batch endpoint creates N executions in one round-trip with partial-failure containment, reusing the single-execute path so per-message routing/trace/dedup are intact; the opt-in dedup window (noetl.subscription_dedup, bounded-by-age, race-safe INSERT … ON CONFLICT, scoped by subscription, subscription.message.deduplicated audit, default off β€” RFC Β§10 OQ1); validation of the new dispatch.batch_dispatch/batch_max/dedup/limits spec blocks; noetl_execute_outcomes_total + noetl_execute_batch_size. Live on kind: batch 12β†’12 COMPLETED; dedup duplicateβ†’1 execution; direct-curl within/outside-window + dedup-off all green. ai-meta β†’ server 7b217d8. Prior (v3.4.2): #90 Phase 5 gcs/s3 spool credential optional (ADC). Prior: #90 Phase 4 β€” spool config validation + subscription lifecycle-status fix (v3.4.1, server#184+server#185): validate the spec.spool block at registration; lifecycle-status reconstruction now matches only the six lifecycle event types so spool/circuit events (which share the subscription's execution_id) can't 500 subscription_get/activate. Prior: #90 Phase 3 β€” push-ingress config endpoint + push catalog validation (v3.3.0, server#182): mode: push requires an ingress.verify block (hmac_sha256 | bearer | pubsub_oidc; none rejected) + new GET /api/internal/ingress/{listener} (service-account-gated) resolving the verify-secret alias through the Wallet + idempotent subscription registration β€” the gateway's DB-free config source. Live-validated on kind. Prior: #90 Phase 2 β€” kind: Subscription type + lifecycle + pool routing + W3C trace (v3.2.0, server#180): first-class kind: Subscription catalog type (source/mode/dispatch validation, no step-DAG) + event-sourced lifecycle endpoints /api/subscriptions (registerβ†’activateβ†’pause/resumeβ†’drainβ†’deactivate, idempotent register, GET list/get) + execution_pool override on /api/execute routing the whole run to noetl.commands.<pool>.<eid> (persisted in playbook_started meta, orchestrator reads back) + W3C trace into meta.trace + command notification + child inheritance. Startup-seeds the subscription resource kind; decodes noetl.event.created_at as TIMESTAMP. Live E2E green. Prior (v3.1.0): subscription ToolKind. Prior: Round-trip JSON null in whole-object {{ step }} references (v3.0.6, server#177; closes noetl/ai-meta#89): a single-expression {{ step }} reference to a prior result envelope carrying a null field rendered that field as the JS token undefined via minijinja's map repr (json_value_to_minijinja maps JSON nullβ†’Value::UNDEFINED); render_to_value then failed serde_json::from_str and returned the whole envelope as a raw string, so the consuming python/rhai step received an unparseable str and crashed. Fix adds a | tojson retry to render_to_value (mirrors the noetl-tools TemplateEngine::render_value the server copy had diverged from): a lone {{ expr }} whose plain render is container-shaped-but-invalid JSON re-renders with | tojson, and minijinja_to_json maps undefined/none β†’ JSON null so the field round-trips as null. 5 new regression tests (null in nested + top-level objects, null array element, explicit | tojson no-double-pipe, scalars unchanged); 619 lib + 8 parity green; clippy clean. Kind-validated against the live paginated-api test-server: cursor fixture walks all 4 pages, terminal next_cursor: null handled, validate_results collects 35 (first_id=1, last_id=35, success) β€” matching offset; pre-fix the 4th check_pagination was command.completed error. ai-meta β†’ server 8e17fbe. Prior: Workflow-arc loops advance across iterations + terminate (v3.0.5, server#176; closes noetl/ai-meta#85): two coupled fixes atop the dispatch-guard re-entry layer. (1) Durable event-sourced loop-ctx propagation β€” step-level set: ctx.* loop variables were recomputed per pass and reverted to the workload default (loop thrashed 0,0,1,0,1,2,…); root cause was start's initializer set re-firing every pass in random HashMap order against the loop's advancing set. Fix persists each completion's rendered set: as a ctx.updated event (latest-wins fold in WorkflowState.ctx + build_context overlay), emitted once per completion keyed by the stable completion event_id (the Utc::now()-fallback completed_at is unstable across reconstructions). (2) Loop-exit hang β€” the exit branch was marked step.skipped on a loop-body-completion pass (recency-based branch-point detector missed it), turning it terminal so the is_step_done guard suppressed the exit dispatch; fixed with a structural loop-branch-point test (any step with a back-edge arc). 614 lib tests (+6; 2 verified to fail without their guard); clippy-clean. Kind-validated: counter loop 0β†’1β†’2β†’3 + terminates; real-http offset pagination 4 pages collecting 35. ai-meta β†’ server e519fdc. (Separate finding: the e2e offset/cursor fixtures read response.data.users but the Rust http tool nests it at response.body.users β€” filed as a follow-up.) Prior: Unblock workflow loops + loop.done-gated transitions (v3.0.4, server#175; closes noetl/ai-meta#83 + #84): the fan-in/reduce barrier counted a loop back-edge (check_pagination β†’ fetch_page) as an upstream and deferred the loop head forever β€” fix excludes back-edges via a new forward_reachable helper (genuine fan-in unaffected); and event.name was never populated for arc evaluation so when: {{ event.name == "loop.done" }} never matched (10+ fixtures hung after an in-step loop:) β€” fix injects event.name = "loop.done" into a completed loop step's next-arc context. Found in a full e2e regression re-sweep (19β†’27/36 on kind); landed with e2e fixture fix e2e#39 (duckdb commandsβ†’command, closes #86). Follow-ups #85/#87 filed open. 26 orchestrator tests +2 new; kind-validated. Prior: container-callback insert matches the deployed event schema (v3.0.3, server#173; tracks noetl/ai-meta#43): the container-callback handler emitted its resume call.done via a stale query targeting attempt + id columns that don't exist on the deployed noetl.event (PK (execution_id, event_id)) β€” every watcher POST 500'd with column "attempt" of relation "event" does not exist, blocking the #43 chain. Replaced with an inline INSERT matching the working handlers::events column set; terminal outcome rides in a chk_event_result_shape-conforming result envelope. Kind-val GREEN: kind_validate_container_callback.sh both probes pass (happy_path β†’ succeeded, oom β†’ failed_oom). Prior: container-tool command type contradiction fixed (v3.0.2, server#172; closes noetl/ai-meta#81): ToolSpec.command Option<String> β†’ Option<serde_json::Value> so the container tool kind's K8s-Job-style array command decodes server-side (was a 400 data did not match any variant of untagged enum ToolDefinition) and passes through unchanged to the worker's ContainerConfig.command: Option<Vec<String>>; a scalar stays a JSON string for the shell/db consumers; ToolCall::from_spec forwards the value verbatim instead of wrapping in Value::String. 2 new regression tests (playbook::types 18/18); clippy clean. Kind-val GREEN end-to-end β€” server accepts command: ["/bin/sh","-c"], worker dispatches the container tool, K8s Job reaches Complete 1/1 (pre-fix kubectl get jobs empty). Prior: e2e-sweep cleanup (v3.0.1, server#171; tracks noetl/ai-meta#49): 64 MB result-store PUT body limit (DefaultBodyLimit β€” was rejecting 15 MB+ payloads with HTTP 413); render_pipeline_config stashes set/args/spec/command blocks before Tera rendering; iter namespace map in build_iteration_command; cmd_render_ctx uses command.context override; stripped diagnostic tracing::debug! blocks. All 7 e2e sweep playbooks PASS on Rust-only kind stack. Prior: Sequential-mode iterator dispatch (v2.62.0, server#166; closes noetl/ai-meta#76): LoopMode enum (Sequential default / Parallel); LoopSpec.mode parsed from loop.spec.mode YAML; StepInfo.iterations_dispatched tracks command.issued count for the sequential dispatch guard; sequential pattern dispatches iteration 0 at fan-out then on each command.completed dispatches next if iterations_dispatched == iterations_completed(). Default is Sequential β€” existing playbooks without explicit spec.mode get sequential behavior. 3 new tests; lib pass; clippy clean. Kind-val GREEN: test/loop COMPLETED 5/5 + iterator_save_test COMPLETED 4 steps. First Claude-direct Rust PR under agents/rules/handoff-routing.md. Prior: Durable result-store endpoints (v2.58.0, server#160; closes noetl/ai-meta#70): PUT + GET /api/result/<eid>/<step>/<ref>. Kind-val GREEN: output_select_test reached playbook.completed. Prior: Rust orchestrator exclusive-routing fix β€” step.skipped for untaken siblings (v2.57.2, server#159; closes noetl/ai-meta#67): under mode: exclusive only one arc fires; pre-fix, the static planner declared the untaken sibling's target as an upstream of any downstream merge step, then the R4 fan-in barrier waited for it forever. Three-part fix: (1) evaluator::evaluate_next_transitions stops break-ing on exclusive-mode match β€” surfaces unmatched siblings as not_matched_with_target results; (2) orchestrator::process_in_progress two-pass refactor β€” emit step.skipped for ALL unmatched arc targets first, then dispatch matched (HashMap-order-independent); (3) +2 unit tests + new defensive Jinja regression guard (server lib 568/0/0). Kind-val GREEN: e2e/fixtures/playbooks/comprehensive_test.yaml reaches playbook.completed in ~4s (was hanging forever). Single-commit patch on top of v2.57.1. Prior: Rust orchestrator step.data template accessor fix (v2.57.1, server#158; closes noetl/ai-meta#66): WorkflowState::build_context now injects a self-referencing .data key on the extracted user_data so {{ step.field }} (existing flat path) AND {{ step.data.field }} (new wrapped path) both resolve. Guarded by !map.contains_key("data") to preserve task_sequence flatten back-compat (a labeled sub-task's data field stays addressable as both <step>.<label>.data.x AND <step>.data.x). 2 new unit tests; cargo test lib 566/0/0; release build clean. Single-commit patch on top of v2.57.0. Prior: Phase D R5 R7 β€” cross-server parity harness; Replay engine port complete (v2.57.0, server#157; closes server#148; tracks noetl/ai-meta#49 Phase D R5). Final slice β€” hermetic parity rig: events.json (13 synthetic events) + expected.json (Python's pre-recorded fold output) + regenerate_expected.py (standalone Python script β€” verbatim extract of service.py fold + helpers, no noetl-package imports). tests/parity_harness.rs 8-test integration suite asserts structural parity field-by-field across all six projections + payload refs. Parity is structural not byte-for-byte hex (Python and Rust hash different digest inputs per R4's design). All 8 tests pass; lib 564/0/0; release build clean. No kind-val needed β€” test-only PR. All 7 Phase D R5 rounds shipped today (v2.51.0 β†’ v2.57.0); the Replay engine port is complete. Phase D R5 R6 β€” payload resolver (v2.56.0, server#156; Refs server#148; tracks noetl/ai-meta#49 Phase D R5): every event's result.reference JSON gets parsed into a typed PayloadSummary and appended to the relevant projection's payload-refs list. PayloadSummary + PayloadRefEntry types mirror Python's dict shapes; extract_payload_ref + payload_summary mirror Python's helpers with three-tier fallback (reference.<field> β†’ rows_ref.meta.<field> β†’ rows_ref.ipc.<field>). ReplayExecutionState.payload_refs + ReplayFrameState.output_ref/output_ref_summary + ReplayBusinessObjectState.payload_refs/last_payload_ref all populated. 15 new unit tests; lib 564/0/0. Kind-validated against live execution showing 3 populated payload_refs with real SHA-256 digests. Only R7 remains. Phase D R5 R5 β€” snapshot seed + base_state + upcaster digest (v2.55.0, server#155; Refs server#148; tracks noetl/ai-meta#49 Phase D R5): the replay fold can now start from a prior fold's output and continue from there. ReplaySnapshotSeed mirrors Python's frozen dataclass; ReplaySnapshotInfo is the output subset; ReplayFoldOptions carries the optional inputs; new fold_replay_state_with_options entry point + 5-arg fold_replay_state as a back-compat shim. R5-introduced ReplayState fields (replay_snapshot, upcaster_registry_digest) both skip_serializing_if None β€” default folds produce R1–R4-identical JSON. Snapshot-storage backend deferred to a downstream sub-issue. 8 new unit tests; lib 549/0/0. Kind-validated wire-shape back-compat. Phase D R5 R4 β€” typed Checksum + projection_checksums (v2.54.0, server#154; Refs server#148; tracks noetl/ai-meta#49 Phase D R5): every replay fold now produces a typed Checksum over the full state + a 6-entry projection_checksums map. ChecksumType enum (initial variant Sha256) + Checksum { type, value } struct + stable_json_bytes deterministic JSON encoder + compute_checksums per-projection + top-level digest run at the end of fold_replay_state. Design decision: digest input is the typed Rust state directly, not Python's flat-row normalize layer; Python byte-for-byte parity deferred to R7. 9 new unit tests; lib 541/0/0. Kind-validated: top-level checksum 41265876487f...ae426; six projection_checksums entries all populated. Phase D R5 R3 β€” loops + business_objects projections (v2.53.0, server#153; Refs server#148; tracks noetl/ai-meta#49 Phase D R5): third slice of Phase D R5 β€” replay fold populates the last two per-projection maps. Two new typed state structs (ReplayLoopState, ReplayBusinessObjectState) replace R2's serde_json::Map placeholders; ReplayState.{loops,business_objects} flip to BTreeMap for deterministic key ordering. Two new ID extractors mirror Python's _loop_id / _business_object_identity resolution order; business_object_status helper mirrors Python's _business_object_status suffix-derived ACTIVE/DELETED transitions; two new populate functions with full event-shape coverage (loop counters bump on command.{completed,failed} + loop.shard.{done,failed} + loop.{done,fanin.completed}; business-object attributes REPLACE/PATCH from meta). payload_refs deferred to R6. 13 new unit tests; lib 532/0/0. Kind-validated: re-probe returns loops + business_objects empty (expected β€” fixture doesn't emit those events). R4 design captured (per user direction): typed ChecksumType enum + Checksum { type, value } struct; future checksum types slot in via enum. Phase D R5 R2 β€” stages + frames + commands projections (v2.52.0, server#152; Refs server#148; tracks noetl/ai-meta#49 Phase D R5): second slice of Phase D R5 β€” replay fold populates stages + frames + commands projections. Mirrors Python's state["stages"] / state["frames"] / state["commands"] per-projection dicts. ReplayEventRow extended with stage_id / frame_id / command_id / worker_id / aggregate_type / aggregate_id / meta columns (all #[sqlx(default)]); three new typed state structs (ReplayStageState / ReplayFrameState / ReplayCommandState); ReplayState.{stages,frames,commands} flip from serde_json::Map to BTreeMap for deterministic ordering; three new ID extractors mirror Python's resolution order; three new populate functions with full status transitions (stage opened/closed; frame dispatched/started/committed/failed/abandoned; command full lifecycle). 10 new unit tests; lib 518/0/0. Kind-validated: re-probe of prior fanout_reduce execution returns commands map populated with 4 entries carrying worker_id + issued_event_id + last_event_id. Phase D R5 R1 β€” Replay endpoint scaffold + execution projection (v2.51.0, server#149; tracks server#148): opens Phase D Round 5 (Python's noetl/server/api/replay/service.py ~1236 LoC β†’ Rust). Sub-issue server#148 documents the 7-round decomposition (R1 scaffold + execution / R2 stages+frames+commands βœ… / R3 loops+business_objects / R4 typed Checksum + projection_checksums / R5 snapshot seeds / R6 payload resolver / R7 cross-server parity harness). R1 ships new GET /api/replay/state route mirroring Python's endpoint.py byte-for-byte (query params + defaults + projection enum + mutually-exclusive cutoffs returning 400); new services::replay module with ReplayService + ReplayCutoff + ReplayProjection + ReplayState + ReplayExecutionState + pure deterministic fold_replay_state; minimal execution projection fold reuses the Phase D R4 terminal-event short-circuit. Phase D R4 follow-up: status endpoint short-circuits on terminal events (v2.50.1, server#147; closes server#146): ExecutionService::get_status now (1) looks up playbook.completed / playbook.failed FIRST and returns COMPLETED / FAILED directly, and (2) accepts 'success' lowercase in the completed_steps counter. Kind-validated: prior execution flipped RUNNINGβ†’COMPLETED on the same DB data. Phase D R4 slice 2 β€” apply_event handles step.skipped (v2.50.0, server#145; closes server#144): new step.skipped arm in state::WorkflowState::apply_event records the step with StepState::Skipped β€” fan-in barrier no longer defers forever when an upstream's when guard evaluates false. Container Tool Callback umbrella #43 Round 2 β€” POST /api/internal/container-callback/{execution_id}/{step} (v2.48.0, server#141; closes server#140; tracks noetl/ai-meta#43): external K8s watcher (Round 1, ops#166) POSTs Job terminal-state events here; handler validates path params, checks staleness via a single indexed SELECT on noetl.event, and emits a call.done event with the structured terminal state on match (or bumps noetl_container_callback_stale_total + returns 202 if no events exist for the execution). Six TerminalState variants survive in meta.terminal_state so playbooks can branch on the specific failure reason. Two new counters; 7 new unit tests; lib 487/0. Round 2 lands first per the umbrella's recommended ordering β€” smallest blast radius; unblocks Round 1 (watcher Deployment) + Round 3 (tools#36 β€” Tool::Container). Secrets Wallet #61 cloud-specific dynamic-secret providers shipped β€” umbrella feature-complete (v2.45.0 server#137 + v2.46.0 server#139 + v2.47.0 server#138): 6d.1 AWS STS AssumeRoleWithWebIdentity (EKS IRSA path; no SigV4 β€” token IS the credential); 6d.3 Azure AAD client-credentials (off-cluster non-IMDS path; sovereign-cloud overrides via env); 6d.2 GCP iamcredentials.generateAccessToken (workload-identity impersonation of a target SA). All three return SecretValue.expires_at populated β€” Phase 6d's cache_decision clamps cache TTL accordingly; Phase 7c.3 background refresh re-resolves inside the window. 39 new unit tests across the three providers; lib 470/0. Secrets Wallet umbrella is feature-complete: envelope encryption + KMS + 5 static-secret providers (GCP-SM, K8s, Vault, AWS-SM, Azure-KV) + 3 dynamic-secret providers (AWS-STS, GCP-IAM, Azure-AAD) + residency policy + cross-region broker + KEK rotation + audit + auto-renewal with stampede collapse. Phase 7c.3 β€” background-refresh wire-up + stampede collapse (v2.44.0, server#136): cache-hit path now spawns a background tokio::spawn to re-resolve via the provider + update the cache via KeychainService::set when the row is inside the refresh window. Cached value returns IMMEDIATELY to the caller β€” worker fetches stay on the fast path. Stampede collapse via new src/services/keychain_refresh.rs RefreshInflight (Arc<tokio::sync::Mutex<HashSet<(i64, String)>>>); concurrent refreshes for the same (catalog_id, alias) collapse to one provider call. Refactor: extracted resolve_via_provider from try_resolve_keychain so cache-miss inline + background refresh share identical code. Phase 7c series wire-complete (7c + 7c.2 + 7c.3). Phase 7a.2 / 7b.2 / 7c.2 β€” operator-facing rotation + audit storage + cache-refresh primitive (v2.42.0 server#127 + v2.43.0 server#129 + server#131): 7a.2 wraps the Phase-7a rewrap_storage_string primitive with POST /api/internal/wallet/rotate-kek (batched cursor scan of noetl.credential + noetl.keychain, RotateSummary { processed, rewrapped, skipped, failed, last_id } for progress checkpointing) and GET /api/internal/wallet/key-status (per-version row counts). 7b.2 adds the noetl.secret_audit table (CREATE TABLE IF NOT EXISTS at startup; server-owned), DbAuditSink impl, and GET /api/internal/secret-audit?credential=&execution_id=&from=&to=&limit= (bounded; ORDER BY occurred_at DESC; hard cap 10_000). 7c.2 adds KeychainService::should_refresh(catalog_id, keychain_name, execution_id, scope_type, now) β€” cache-side companion of the Phase-7c decision primitive; reads the row's expires_at, asks secrets::dynamic::should_refresh_default (honours KEYCHAIN_CACHE_REFRESH_WINDOW_SECS), bumps noetl_secret_refresh_total{outcome="triggered"} on true. Backward compatible. Next: 7c.3 stampede-collapse mutex + background re-resolve; 6d.1/.2/.3 cloud-specific dynamic providers. Phase 7c β€” token auto-renewal primitives (closes Phase 7) (v2.41.0, server#125): final named round of the Secrets Wallet umbrella. secrets::dynamic::should_refresh(expires_at, refresh_window, now) decision primitive (true iff expires_at set + still valid + within refresh window); KEYCHAIN_CACHE_REFRESH_WINDOW_SECS env (default 60). noetl_secret_refresh_total{outcome} counter (triggered|succeeded|failed|stampede_collapsed; failed alert-worthy) + noetl_secret_refresh_duration_seconds histogram (50ms–5s buckets). 5 new unit tests; lib 427/0. Lib-only. All named phases (1–7) of the Secrets Wallet umbrella are complete. Remaining queue is discrete follow-up sub-issues only. Phase 7b primitives β€” secret-resolution audit service (v2.40.0, server#123): durable audit trail of every credential resolution. AuditEvent struct (NEVER contains the secret value); bounded Operation + Outcome enums; AuditSink trait + NoopAuditSink default + SecretAuditService wrapper with record_async (fire-and-forget, never blocks resolver) + record_strict (await, used when compliance mandates the row exist before the value releases) + record (dispatches by strict-mode). NOETL_SECRET_AUDIT_REQUIRED env (default false; 1/true/TRUE/yes/YES enable strict). noetl_secret_audit_writes_total{operation, outcome, status} counter (failed_strict alert-worthy). 8 new unit tests; lib 422/0. Lib-only. Phase 7a β€” KEK rotation primitives (v2.39.0, server#121): starts Phase 7. KeyManager::current_key_version() trait accessor; EnvelopeCipher::rewrap_storage_string primitive (parse β†’ if same version Skipped; else unwrap with historical version β†’ re-wrap with current β†’ Rewrapped { old_key_version, new_key_version, new_storage_string }). Plaintext payload NEVER reconstructed β€” pure DEK re-wrap, AES-GCM ciphertext bytes stay byte-identical. noetl_wallet_rotate_total{table, status} counter (skipped|rewrapped|failed_unwrap|failed_wrap|parse_error). 4 new unit tests; lib 414/0. Lib-only. Phase 6e β€” cross-region broker (closes Phase 6) (v2.38.0, server#119): BrokerRegistry (region β†’ broker_url from NOETL_SECRET_BROKER_REGISTRY env; empty default = pre-6e fail-closed); POST /api/internal/cross-region/resolve peer endpoint validates expected_entry_region == server_region() (defensive against stale peer registries), resolves locally, seals via Phase-5a primitives to the requesting worker's pubkey directly; get_sealed handler falls back to broker on AppError::ResidencyViolation; KeychainDef.no_broker_fallback per-credential opt-out; AppError::CrossRegionUnreachable β†’ HTTP 502. Two new metrics: noetl_secret_broker_call_total{broker_region, outcome} + noetl_secret_broker_call_duration_seconds{broker_region} histogram (50ms–5s). 10 new unit tests; lib 410/0. Both residency shapes operational: hard isolation (strict + no broker = HTTP 403) + soft federation (strict + broker registered = transparent cross-region routing). Phase 6 closes. Phase 6d primitives β€” dynamic-secret support + cache honors issuer TTL (v2.37.0, server#117): SecretValue.expires_at: Option<DateTime<Utc>> field; src/secrets/dynamic.rs cache_decision() honors min(default_ttl, expires_at - now - safety_margin) and returns SkipCacheAlreadyExpired when the deadline is already past or inside the operator's safety margin; KEYCHAIN_CACHE_DYNAMIC_SAFETY_MARGIN_SECS env (default 60); resolve_keychain_entry_with_meta returns the bundle's earliest expires_at; CredentialService::resolve_via_provider consumes the helper. Two new metrics: noetl_secret_dynamic_ttl_seconds histogram (1m/5m/15m/1h/4h/12h buckets) + noetl_secret_cache_skip_total{reason} counter. 7 new unit tests; lib 398/0. Backward compatible (providers without expires_at keep the 600 s default). Phase 6c β€” residency-policy gate (v2.36.0, server#115): KeychainDef.residency enum (none|advisory|strict, default none) + allowed_regions allowlist; resolver runs the gate at the top of resolve_keychain_entry BEFORE any provider call so strict-mode mismatches short-circuit with AppError::ResidencyViolation (HTTP 403, clear "credential X is region-locked to Y; this server is in Z" message that NEVER includes the value). noetl_secret_residency_check_total{policy, decision} counter β€” strict + violation_blocked is alert-worthy, advisory + violation_allowed is the migration-window signal. 8 new unit tests; lib 391/0. Phase 6b β€” ProviderRegistry + per-(provider, region) metrics (v2.35.0, server#113): server-side cache of (provider_id, region) β†’ Arc<dyn SecretProvider> so the resolver doesn't rebuild from env on every cache-miss; RwLock + double-checked locking on the build path so concurrent get_or_build for the same key only builds once. Optional TTL via NOETL_SECRET_PROVIDER_TTL_SECONDS. New noetl_secret_provider_build_total{provider,region,status="cache_hit|ok|error"} counter + noetl_secret_resolve_duration_seconds{provider,region} histogram (5 ms – 5 s buckets). 7 new unit tests; lib 383/0. Phase 6a β€” region tag on keychain entries + per-region routing (v2.34.0, server#111): starts Phase 6 (residency-aware distributed resolution). KeychainDef.region optional field (no schema migration β€” lives in existing JSON blob); SecretRef.region provider-agnostic; AWS provider consumes it as the regional endpoint with explicit precedence (<region>: ref prefix > field > legacy project overload > AWS_REGION env). New NOETL_SERVER_REGION env + server_region() / effective_region() fallback helpers. noetl_secret_resolve_total{provider,region,status} counter per observability.md Principle 1. 5 new unit tests; lib 376/0. Lib-only β€” backward compatible. Phase 5b β€” wire format + sealing endpoint (v2.33.0, server#107): new GET /api/credentials/{id}/sealed?worker_id=<name> returns a SealedEnvelope (X25519-sealed credential JSON) addressed to the named worker; workers opt in by including worker_public_key in their register payload's runtime JSON blob β€” no schema migration; 400 BadRequest when the worker_pool row exists but didn't register a key; noetl_credentials_sealed_total{status} counter + credential.seal span per observability.md. Kind-validated end-to-end (Python cryptography + HKDF + ChaCha20-Poly1305 opens the envelope β†’ recovers the bearer token + scope round-trip). Phase 5a β€” sealed payload crypto primitives (v2.32.0, server#107): src/crypto/sealed.rs X25519 ECDH + HKDF-SHA256 + ChaCha20-Poly1305 sealed-box (nonce derived from the shared secret, AAD pins alg+v for clean alg-mismatch rejection); 12 unit tests (round-trip, tamper, alg/version-mismatch, JSON wire stability); lib 369/0. Defense-in-depth on top of Phase-4 mTLS β€” cleartext never enters the response body. Lib-only; 5b adds the runtime-registry worker pubkey + sealing endpoint, 5c the worker side. Providers 3.x β€” AWS Secrets Manager + Azure Key Vault (v2.31.0, server#105): two new backends behind the one SecretProvider trait completing the 5-provider matrix. AWS SM uses hand-rolled AWS Signature Version 4 signing (no aws-sdk dep tree; signing key verified by a unit test against AWS's published reference vector); ref shape [<region>:]<secret-id>[#<json-key>] with JSON-key extraction for multi-field secrets; creds from env (the IRSA-injected triple). Azure KV uses IMDS Managed Identity (AKS/VMs) with TTL-cached bearer; ref shape [<vault>/]<secret-name>[#<version>]; sovereign clouds via NOETL_AZURE_KEYVAULT_DNS_SUFFIX. 21 new unit tests; lib 357/0; cloud-only backends (kind-val at unit-test layer like GCP). Phase 4a β€” opt-in TLS/mTLS listener (v2.30.0, server#103): the worker↔server credential channel (GET /api/credentials/<alias>) was plain HTTP; opt-in TLS via NOETL_TLS_CERT+NOETL_TLS_KEY (+NOETL_TLS_CLIENT_CA β‡’ mTLS), ring rustls + axum-server bind_rustls, kind-validated (200 w/ client cert, rejected w/o, plain HTTP refused). Providers 3.x β€” HashiCorp Vault provider (v2.29.0, server#101): a provider: vault keychain alias resolves from a Vault KV v2 secret (X-Vault-Token; ref [<mount>/]<path>#<key>), kind-validated end-to-end against an in-cluster Vault β€” second backend validatable on kind after K8s. /api/executions list perf + status fix (v2.28.1, server#99, #62): candidate-first rewrite (start-event index, not a 3.2M-row seq scan) β€” 6.5 s β†’ 0.015 s (~430Γ—), identical list; bool_or status-drift fix (was all-RUNNING). Secrets Wallet #61 providers 3.x β€” Kubernetes Secrets provider (v2.28.0, server#97): a provider: k8s keychain alias resolves from an in-cluster Secret via the API server + ServiceAccount token + cluster CA β€” the first secret backend kind-validated end-to-end with a real value (GCP needs GKE). Orchestrator-strand fix (v2.27.2, server#95): a deterministic evaluate failure (an invalid template in a step code body, an unknown step in a next arc, malformed routing) now emits a terminal playbook.failed instead of stranding the run in RUNNING forever β€” surfaced by the #54 e2e sweep (closed server#94). Parser fix (v2.27.1, server#93): NextSpec untagged-variant order β€” the list form next: [{step: x}] was deserialized into a struct Router positionally, silently dropping its arcs (and defeating unknown-step validation); sequence-shaped variants now precede the struct. Secrets Wallet Phase 3c β€” keychain cache (v2.27.0, server#91): execution-scoped, envelope-encrypted, TTL'd cache so an auth: "{{ alias }}" lookup isn't re-fetched from the secret manager per step; + fixed the keychain storage layer (queries never matched the table β€” also repairs the /api/keychain endpoints). Phase 3 (resolution) complete β€” R3b (v2.26.0) resolves a provider: gcp keychain alias from GCP Secret Manager on a credential miss; built on R3a/R2/R1 (v2.23.0–v2.25.0). Phases 1–2: Cloud KMS for the KEK (v2.22.0); envelope encryption (v2.21.0)
noetl/worker Rust worker + EHDB command-bus writer host v5.123.0 released Β· v5.123.0 in prod (both system pools on Secret-Manager files, 2026-08-27; event-log tier primary and SERVING) πŸ›‘οΈ The writer seals BOTH logs on SIGTERM, and stops ingest first (2026-08-03) β€” worker#211 ddb4de8, v5.92.0. A fail-loud internal bus source (closes #221 β€” unset/blank/typo'd *_SOURCE on an enabled consumer is now a startup error, not a silent fall-through to the deleted NATS transport), the sequenced seal plus a confirmed stop-ingest (#209 graceful half), and face supervision (detection-only mitigation for ehdb#311). The seal took two rounds: the first had the right order but signalled with Notify::notify_waiters(), which stores no permit while the acceptor registers on its first poll β€” so a shutdown firing early lost the wakeup and the ingest listener was never closed (lsof caught the writer's own acceptor still in LISTEN after shutdown.run() returned; 24/30 full-suite runs under contention). c44ebf2 makes it a watch plus an ack after the fd is dropped. Prod gate: controlled SIGTERM sealed hosts=2 in 568 ms, every cursor resuming clamped=false / replay_records=0; a synthetic execution moved the bus +19 and all three group cursors +31 with exactly 31 durable events, lag 0. ⚠️ semantic-release then rewrote Cargo.toml back down to 5.91.3 on main (#224) β€” the tag, the image and the ai-meta pointer (ddb4de8) are correct. Prior: 🟒 T5-readiness deploy (2026-07-31) β€” the writer now serves the per-pool lag ehdb_feed_subject_lag{subject} (the user pool's KEDA trigger, worker#197), the #208 resume facts, and ehdb_l0_out_of_order_appends (worker#198, closes #206) β€” all from one scrape. Prior: πŸ” v5.81.3 β€” the writer survives a restart, and the bus got faster (2026-07-30) β€” worker#196 resumes from a durable committed cursor clamped to the reopened log's tip and seals on SIGTERM (terminationGracePeriodSeconds 30 β†’ 90 s); claim sockets get TCP keepalive + a negotiated coordinator heartbeat, so a dead or stuck writer surfaces as an ordinary error instead of an indefinite park. worker#195 adopts the ehdb group-commit + off-lock fsync. Prod gate: writer deleted mid-load β†’ 38/38 COMPLETED, 114 = 38Γ—3, 0 dup / 0 loss, ~2.7 s redial (#208 CLOSED). Prior: πŸš€ L1 T4 β€” the worker hosts the EHDB command-bus writer in PROD (2026-07-27) β€” spawn_writer_host (ingest :9100 / claim :9101 / /metrics :9102) on a premium-rwo PVC; workers claim over the network with subject filters (0 cross-pool claims proven on prod). v5.81.1 carries the #203 runtime fix (the writer lives here). v5.79.0/v5.80.0/v5.81.0 = the #199 write-behind sink slices (durable-segment GC gate / sink-confirmation signal / auto-sink observe-only), all default-off. Prior: 🩹 #166 Phase 1 β€” bounded off-server WAL index, system-pool OOM CURED on prod (2026-06-30) β€” slim chain + LRU/TTL/byte-ceiling eviction + cold-rebuild-on-miss, all flag-gated to today's unbounded behavior (worker#144, v5.49.0 4e163b9; ops env #231). Prod system-pool flags-on (SLIM/TTL_SECS=900/MAX_BYTES=256Mi ledger/REHYDRATE_ON_MISS/SWEEP_SECS=30): idle RSS 1265Mi β†’ 67Mi holding, TTL swept 579 idle execs, drives served, /livez 200, login + "Trip to Paris" turn COMPLETED, 0 OOMKills. 2Gi limit kept as headroom (#163). Rollback = flags off β†’ :5.48.1. Prior: 🩹 #161/#163 state_builder self-heal + /livez backstop (worker#143, v5.48.1 cc9ae22): WAL drain recreates a dead JetStream consumer + fails /livez after an unable-to-serve window so K8s restarts the wedged pod. Prior: ⚑ #156 off-server drive tail-attach DEPLOYED on prod (2026-06-29) β€” resolve_offserver_orchestrate_input applies the server-attached tail_events to the pool-side WAL index BEFORE the build loop, so a warm-index hop serves on the first attempt regardless of drain lag β†’ per-hop O(execution), no 8s reconcile cliff; additive + idempotent, insufficient tail falls through to today's drain/reconcile path (worker#142, v5.48.0 12710b6; prod flag-on: 90 drives attached, O(tail) mean 24 ≀64, all served 0 fallback, 0 restarts, reconnect 0/30s; 1-step rollback). Prior: πŸš‘ #157 hotels-turn drive wedge fix (worker#141, v5.47.3 9f83ab5): __orchestrate__ drive result stays inline regardless of size. Prior: πŸ†• G2 container poll-fallback MERGED + RELEASED (review-only) β€” watcher-free poll-to-terminal completion fallback for long-running k8s Jobs, flag-gated NOETL_CONTAINER_COMPLETION_POLL default-off (worker#139, v5.47.0 32b0e96; ai-meta pointer 2e79af6). Prior: πŸš€ #130 off-server drive latency fix SHIPPED + DEPLOYED TO PROD ON OFF-SERVER (fix(offserver): event-signalled drive + per-message WAL index lock release; re-released via trivial PR worker#134 after the dropped webhook; ghcr.io/noetl/worker:5.46.1 @ sha256:97aa2690…; ai-meta pointer 32e95f4; warm drive ≀0.1s, no offserver_retry, event_scans=0, CQRS sole-writer + result-tier intact, 0 restarts). Prior: πŸ“¦ #104 Phase E + F MERGED β€” side-effect durability barrier + result-tier DR re-derive (NOETL_SIDE_EFFECT_BARRIER E adopt-only exactly-once side effects, worker#130 β†’ v5.44.0 d696f7e, depends on published noetl-tools 3.17; NOETL_RESULT_TIER_DR F materializer verify-and-repair byte-identical, worker#131 β†’ v5.45.0 dd07016; both default-off, inert in prod). Prior: πŸͺΆ #104 Phase D MERGED β€” the minting flip (NOETL_RESULT_MINT_AUTHORITATIVE, default off: the result materializer becomes the authoritative tier writer (implies the Phase B flag) + resolve-by-URN becomes the primary consume read path (implies the Phase C flag), with the dual-written result_store as the fail-safe fallback; new noetl_worker_result_mint_authoritative_total{path} = tier
noetl/tools Shared tool registry crate v3.25.0 🧩 provider tool round 5 β€” wire-shape eventlog-tier fold adapter + stack-scoped ownership MERGED + RELEASED (tools#90, v3.25.0 5a06c7e, crates.io-tarball-verified β€” all 5 new pub fns present; ai-meta pointer toolsβ†’5a06c7e; #189 OPEN): extract_facts_from_tier_records/fold_tier_records consume the EventLogRecordView rows from GET /api/ehdb/tiers/eventlog β€” unwrap the event body from the payload string + a bounded recursive search for the fact. Bug fixed: the round-3/4 extract_facts_from_eventlog fixed paths never matched the real call.done nesting (context.result.context.data.provider_fact), so a fold over real event data on 3.23.0/3.24.0 silently returned nothing (only ever run on synthetic records; exposure latent β€” the HTTP adapter didn't exist until round 5). Stack scoping: OwnershipModel::for_stack/stacks, detect_orphans_scoped (fixes detect_orphans folding all stacks into one model), detect_stack_conflicts (URN owned by >1 stack without an intervening delete; transfers not flagged). +3 tests (55 provider / 480 lib), clippy --all-features clean, no libduckdb compile. Prior: 🧩 provider tool round 4 β€” reconcile policy + confirm-gated drift adoption MERGED + RELEASED (tools#89, v3.24.0 c172efb, crates.io-verified; ai-meta pointer c6790983β†’toolsβ†’c172efb; #189 OPEN): a reconcile: policy on the provider step β€” report (new default, changes nothing: no cloud mutation, no ownership fact), enforce (round-2 converge), adopt (accept live actual as the new desired, read/GET only β€” zero cloud mutation, structural). adopt confirm-gated identically to destroy (dry-run field-by-field drift diff + plan_digest; apply needs confirm:<digest>; blind apply + stale digest REFUSED, no state written); covers update-in-place + import (Untrackedβ†’Owned). Ships the pure query-adapter halves (normalize_live + provider_state::extract_facts_from_eventlog/fold_eventlog) + drift_between. Behavior change (user-confirmed): converging now needs reconcile: enforce. 52 provider + 477 lib tests, clippy --all-targets clean, no libduckdb compile. Prior: πŸš€ provider tool round 3 β€” confirm-gated destroy + EHDB ownership projection + endpoint override MERGED + RELEASED (tools#88, v3.23.0 aa32938; ai-meta pointer f6eeb479; #189 OPEN): Fork 2 destroy β€” folders.delete/projects.delete/services.disable/{organizations,billing_accounts}.iam.remove_binding; a dry-run resolves the target against live state (id/etag/lifecycle/presence) and emits an explicit plan + SHA-256 plan_digest; apply needs confirm:<digest> β€” blind apply + stale confirm REFUSED, nothing deleted; no wildcard/glob/bulk; needs auth: even to plan; real destroys are a human step (wiremock only). Fork 1 ownership β€” secret-free provider_fact per mutating converge + pure noetl_tools::provider_state fold (fold_facts/compute_drift/detect_orphans), consuming the raw eventlog tier (/api/ehdb/tiers/eventlog) not the secret-free /api/ehdb/events read-model (#178 collision resolved, no parallel store). Endpoint override β€” config-level endpoint: reroutes onto a mock/emulator for offline noetl exec --runtime local validation. 39 provider tests (incl. stale-digest-refused, blind-apply-refused, drift/orphan) + 464 lib + clippy clean; wiki Provider-Tool page. Follow-up: query I/O adapter + noetl provider {plan,drift,orphans} CLI; round-4 = drift adoption. Prior: πŸš€ provider tool round 2 β€” apply-mode converge + bounded LRO MERGED + RELEASED (tools#87, v3.22.0 eb3d8c7; ai-meta pointer 6cd1ba38): the 4 multi-step ensure/ensure_binding actions (+ services.enable/projects.link) became real, idempotent GET-first converge β€” read actual, write only if absent, re-run = no-op (changed:false), stateless (no state store); IAM ensure_binding read-modify-write with etag preserved. Bounded inline LRO polling for folder/project create Operations (poll.* bound; descriptive timeout naming the op + resume path; callback overflow documented, not built). wiremock tests (converge + no-op-on-rerun + LRO timeout); 24 provider tests + clippy clean; wiki Provider-Tool page. Scope held: no delete verbs, no drift layer, no live cloud. Prior: πŸ†• new kind: provider cloud provider tool (Google REST MVP) MERGED + RELEASED β€” executes the typed Google API operation specs (Cloud Resource Manager v3 / Cloud Billing v1 / Service Usage v1 over reqwest + GcpAuth ADC; zero new heavy deps; rust-sdk deferred β†’ REST) the gcp-org-playbooks emit instead of gcloud; plan vs apply (dry_run default true β†’ would_call echo with no network; apply requires an explicit auth: alias, no ambient ADC); single-request actions execute, multi-step ensure/ensure_binding plan-able + apply-stubbed; secret redaction; aws/azure cross-cloud seam; no worker change; 13 unit tests (tools#86, v3.21.0 cd9d71c; ai-meta pointer e9b3e14e; umbrella #189 OPEN). Prior: πŸ†• G1 container GPU placement + G2 poll helper MERGED + RELEASED β€” node_selector/tolerations/volumes on the container tool + a poll-to-terminal helper (tools#82, v3.19.0 f36d020; ai-meta pointer 2e79af6). Prior: πŸ“¦ #104 Phase E MERGED β€” registry::kind_is_side_effecting side-effect classifier the worker durability barrier consumes (conservative default true; only noop/rhai false) (tools#78, v3.17.0 1d49dd5; members noetl-directives + noetl-locator re-published first). Prior: πŸ“¦ #104 Phase B β€” ResultCoordinates::parse/from_locator on the slim noetl-locator (the inverse of logical_uri β€” recover (tenant, project, execution_id, step, frame, row, attempt) from the worker-stamped URI). Additive, so the member bumped to noetl-locator 0.1.1 (published) + noetl-tools v3.16.0 7da39d8 re-exports it (tools#77). Prior: πŸ“¦ #104 Phase A β€” extract the slim dependency-free noetl-locator crate (noetl-locator 0.1.0, pure std); noetl-tools re-exports it as noetl_tools::locator (tools#76, v3.15.0 dc0c5d8): the Resource Locator (ResourceLocator, ResultCoordinates, shard_key, CellPlacement, legacy-ref parse) is now a lean workspace member so the control-plane noetl-server can parse the canonical result URI without pulling noetl-tools' heavy graph (duckdb/kube/arrow/tonic/rhai). feat(locator): subject β†’ noetl-tools minor + the release CI published the new member noetl-locator 0.1.0 to crates.io (member-publish order: locator before the root). Worker stamp path (noetl_tools::locator::ResultCoordinates) unchanged. Mirrors the lean noetl-directives member (#92). Prior: βœ… #95 β€” postgres pg_value_to_json temporal/identity serialization (tools#75, v3.14.2 6d9b674): the postgres tool probed i64/i32/f64/bool/String/json/DateTime<Utc> and fell through to Value::Null for everything else β€” a tz-naive timestamp (NaiveDateTime) column serialized to null even though the value was present (the auth0_login expires_at: null repro). Added arms for timestamp (ISO-8601, no offset suffix β€” a tz-naive value carries no zone), date/time, uuid (hyphenated lowercase), numeric/decimal (exact decimal string via a direct lossless decode of the postgres numeric binary wire format), bytea (base64). 409 lib tests + clippy + a kind-gated live-postgres before/after integration test (tests/postgres_temporal_kind.rs). Prior: βœ… #127 β€” task_sequence per-sub-task context optimization (behavior-preserving) (tools#74, v3.14.1 c8656c1): the drain rebuilt the template context per sub-task (running_ctx.clone() + 2–4Γ— to_template_context() deep-clones + per-block ExecutionContext clones + a fresh context_to_value() per templated field). TemplateEngine::render_value now builds the proxied minijinja context ONCE and threads it through the recursion (render_value_with/render_with; minijinja Value is Arc-backed β†’ reuse is a refcount bump), and new build_context_with_overlay(&variables, overlay) builds straight from &variables + a small overlay, skipping the intermediate HashMap deep-clone. Isolated micro-bench: per-sub-task context cost 2988.9Β΅sβ†’1147.1Β΅s (βˆ’61.6%, 2.6Γ—). 407 lib tests + 2 new equivalence pins + clippy clean. Worker adopts via worker#125 (v5.40.4). Closes #127. Prior: βœ… #125 β€” task_sequence honours all control-flow directives (do: jump/break/retry) (tools#73, v3.14.0 638c3c6): the Rust runner previously matched only "fail"; do: jump/to:, do: break, do: retry (attempts/backoff + infinite-jump guard) now work. βœ… #126 β€” http tool exposes parsed body under data (Python-era contract restored) (tools#72, v3.13.1 8dd0e1f): body was under data.body; now under data with body as a back-compat alias. Together, the two fixes make the 10Γ—1000 batch pft_flow_test pass end-to-end on kind. Perf follow-up #127 filed. Prior: #103 β€” deferred (ack-after-processing) ack (v3.13.0, tools#71): AckMode::Defer in the subscription SourceClient surfaces a durable per-message ack handle (NATS $JS.ACK reply subject) instead of acking inline; SourceClient::ack(ack_ids, AckDisposition) = Ack/Nack/Term (NATS + Pub/Sub); tool operation: ack|nack|term. Opt-in β€” existing callers unchanged. The capability the worker materializer (v5.34.0) drives for ack-after-materialize. Prior: #90 Phase 4 β€” store-and-forward spool engine + per-downstream circuit breaker (v3.4.0, tools#54): noetl_tools::spool β€” circuit breaker (trip/half-open/close, NATS-KV-serializable, per-downstream OQ2), SpoolItem (SHA-256 + noetl://spool ref + recv_seq-ordered keys), nats_object/local_disk backends, ordered-replay engine (global/per_key/none + idempotency + dead-letter + retention/GC). 44 unit tests + real-NATS integration test. Prior: #90 Phase 2 β€” header-directive engine + public build_source factory (v3.3.0, tools#52): source/directives.rs β€” DirectiveSpec/DispatchPlan turn allowlisted message headers into dispatch instructions (redirect dispatch.playbook, dispatch.execution_pool, priorityβ†’pool, idempotency_key, content_type/schema_hint, W3C trace), untrusted by default (allowlist + value-allowlist enforced at parse; multi-value last-wins; applied[] audit). Public build_source(cfg, ctx) so the worker continuous runtime constructs the same SourceClient. 12 new tests. Prior (v3.2.0): bounded-drain subscription tool + SourceClient (Phase 1). Prior: Multi-tool sibling references (v3.1.1, tools#48; closes noetl/ai-meta#87): in a tool: [list] step, TaskSequenceTool stored each sub-tool's result for the aggregated output but never injected it into the running context, so a later sub-tool's {{ <label>.<field> }} rendered empty β€” masked in quoted positions, a syntax error at or near "," in unquoted numeric SQL (save_edge_cases test_large_payload). Fix injects each sub-tool's result under its label (with a synthetic .data self-ref matching build_context) so later siblings + a later python sub-tool's stdin variables resolve it. 2 new unit tests; lib 300/0. Kind-validated: save_edge_cases test_large_payload β†’ record_count = 100, save_delegation_test clean. Worker adopts via worker#69 (b97f642). Prior: e2e-sweep cleanup (v3.1.0, tools#47; tracks noetl/ai-meta#49): YAML boolean when: true in policy rules now checks as_bool() before the string-template fallthrough (Value::Bool as_str() returns None); `
noetl/cli Rust CLI + local-mode runner v4.21.0 (noetl-executor 0.7.0) 🧩 provider round 5 β€” noetl provider {plan,drift,orphans,adopt} CLI verbs MERGED + RELEASED (cli#68, v4.17.0 a2e5b4d, crates.io-tarball-verified + released-binary-proven; ai-meta pointer cliβ†’a2e5b4d; #189 OPEN): the Terraform-style CLI over the kind: provider tool, local mode β€” plan=dry-run, drift=reconcile:report, orphans=detect_orphans_scoped+detect_stack_conflicts, adopt=confirm-gated reconcile:adopt. Glue only β€” reuses ProviderTool::execute + provider_state fold + to_tools_config; URN derivation delegated to the tool; last-known-desired from --server (GET /api/ehdb/tiers/eventlog) or offline --facts-file. Re-pins noetl-tools 3.24β†’3.25 (HARD compile dep); noetl-executor unchanged at 0.7 (cli#68 touches zero executor/ files β†’ no dead surface, confirmed in the published Cargo.toml). Released-binary proof: noetl-v4.17.0-darwin-arm64 from the release pipeline ran all 4 verbs GET-only zero-writes against the mock. Prior: 🧩 provider round 4 β€” local-mode drift adoption + executor 0.7.0 MERGED + RELEASED (cli#66, v4.16.0 7557dde, crates.io-verified; ai-meta pointer cliβ†’7557dde; #189 OPEN): local-mode reconcile/known_desired passthrough through Tool::Provider + tools_bridge + playbook_runner; re-pin noetl-tools 3.23 β†’ 3.24; noetl-executor 0.6.0 β†’ 0.7.0 (round-4 adds reconcile+known_desired public fields β€” 0.6.0 was already on crates.io without them and publish-crate skips existing versions, so the bump keeps the released bin from shipping the surface dead). libduckdb-sys unchanged (1.2.2, no fresh C++ compile). release-cli publish-crate GREEN; crates.io-verified noetl 4.16.0 + noetl-executor 0.7.0 live. Local-mode proof re-run on the published 3.24.0 (mock, no live cloud, no mutation): report β†’ drift diff + no fact; adopt dry-run β†’ diff + plan_digest 9bfc9023…; matching confirm β†’ adopted (GET-only); STALE confirm β†’ REFUSED "No state was written". Prior: πŸ› οΈ release-cli crate-publish FIXED (cli#67, v4.15.1 5bb1705; ai-meta pointer 7d58e7d; #190 OPEN): bumped noetl-executor 0.5.0β†’0.6.0 (+ bin dep "0.5"β†’"0.6") so the added Tool::Provider variant ships to crates.io β€” executor 0.6.0 + noetl 4.15.1 now live (the publish-crate job had failed 2 releases running, crate channel stuck at 4.13.0). apt/homebrew still SKIP (missing repo secrets) but now warn. Prior: πŸš€ provider round-3 local-mode passthrough + re-pin to noetl-tools 3.23.0 (cli#65, v4.15.0 94cc72a; ai-meta pointer cbc7be99; #189 OPEN): extends Tool::Provider + tools_bridge + playbook_runner with endpoint/stack/confirm so the round-3 fields reach the tool in noetl exec --runtime local; re-pins noetl-tools 3.22.0 β†’ 3.23.0 (libduckdb-sys unchanged β†’ no fresh C++ compile). Local-mode PROVEN on 3.23.0 (mock, no live cloud): endpoint-override apply β†’ services.enable ENABLED β†’ changed:false; destroy dry-run β†’ projects.delete resolved live β†’ plan + plan_digest 6b5efdbb… + confirm_hint, no DELETE. Prior: πŸš€ kind: provider dispatches through noetl exec --runtime local (cli#64, v4.14.0 2d46013; ai-meta pointer 32a8fb86): new Tool::Provider enum + tools_bridge dispatch arm + playbook_runner render arm run the same noetl-tools ProviderTool the worker runs β€” toward "replace Terraform, especially in local mode". Re-pins noetl-tools 3.5.0 β†’ 3.22.0 (libduckdb-sys unchanged β†’ no fresh C++ compile); carries + supersedes cli#63 (duckdb passthrough, closed); adds a duckdb-integration passthrough feature to noetl-executor. Local-mode PROVEN: a --runtime local run emitted the real would_call plans (CRM folders.list GET, serviceusage services.enable POST) with the 3.22.0 converge field, and apply mode entered the converge dispatch gated by the explicit-auth rule (mocks, not live cloud). Prior: noetl subscribe β€” local-mode subscription listener (RFC #90 Phase 6) (cli#60, ai-meta β†’ 2fb3fb0): standalone listener + FileEventSink JSONL + local_disk spool; cli-only. Prior: --include-data flag doc fix (cli#58)
noetl/gateway Gatekeeper β€” auth + SSE + push-ingress v3.12.0 released Β· v3.7.1 in prod #90 Phase 3 β€” push-ingress (Mode C) + auth-gated directive trust (gateway#28): POST /ingress/{listener} verifies HMAC / bearer / Pub-Sub-OIDC β†’ only-then directives β†’ one POST /api/execute per delivery on the dedicated pool (verify-and-forward, no DB on the ingress path); verify_then_plan makes the auth gate a testable invariant; first /metrics surface. Live E2E green (HMAC 12/12 + bearer 12/12). Prior (v3.2.0): Phase F R3b-2 shard-info twin endpoint.
noetl/noetl Python CLI wheel on PyPI (PyO3 over the Rust CLI) + legacy Python tree 5.0.0 on PyPI (Rust CLI via PyO3, abi3-py39) / v4.12.1 legacy (ecd16a2) βœ… #115 Phase 2 DDL (noetl#667, ecd16a2): canonical schema_ddl.sql prev_event_id columns on noetl.event + noetl.command + idx_event_prev_event_id for fresh installs (the Rust server also ensures them idempotently at startup). Otherwise deprioritized per Rust-only direction; pytest debt at noetl/noetl#663 parked
noetl/ops Helm + manifests (untagged) πŸͺΆ #104 Phase B β€” NOETL_RESULT_MATERIALIZER_ENABLED (default false) + single-cell seed NOETL_RESULT_CELL_ENV/REGION/CELL on the kind system-pool deployment; prod manifests untouched (ops#203, c92753c). Prior: k8s-watcher durable image + pod-level OOM classification (ops@cacc513, ops#168; closes noetl/ai-meta#80, tracks noetl/ai-meta#43): retired the dead bitnami/kubectl:1.30.3 (removed from Docker Hub; cluster was on the bitnamilegacy stopgap) for alpine/k8s:1.30.3 (kubectl + jq + curl baked in) β€” the prior runtime install never put curl on PATH so callback POSTs returned HTTP 000. classify_pod_failure() now reads the backing Pod's status (RBAC already grants pod reads) to emit failed_oom (OOMKilled) / failed_image_pull (ImagePullBackOff); build_body's completed_at fallback uses RFC3339 `now
noetl/docs Docusaurus site (untagged) ADR Implementation-status block
noetl/travel Reference SPA (domain-fork example) 571bf2d 🩹 SPA auth-timeout + slow-turn robustness LIVE on prod (2026-07-02, bundle index-CCFdWtGQ.js) β€” gateway-auth client timeout 15β†’40s + one silent retry (closes #167) plus friendlyTurnError + checkPlaybookAccess 20s-timeout/retry + a Retry button so a slow turn never surfaces a bare "Load failed" (authorization decisions unchanged) (travel#90 + #91); verified login healthy + "Trip to Paris" renders place_list on the new bundle (no regression); rollback = prior bundle index-Buo8vFKk.js. Prior: πŸ“± Mobile responsive layout shipped to prod (2026-06-28) β€” 3-panel shell reflows to a single column at the md breakpoint with sidebar + Trip-state as drawers (travel#80); deployed via Cloudflare Pages (https://417ab9ee.travel-bgo.pages.dev), verified live at 380px. Prior: all-5-providers-real-widgets (planner v1.5/catalog v51).
noetl/ehdb Event Horizon DB β€” the storage substrate; both buses in prod v0.3.0 Resilience gap is the unsealed tail (RF=1), not replication in general
noetl/signal-mesh Tiered A2A/ReAct agent mesh over the EHDB event log v0.1.0 (untagged) 🧩 split out of ehdb 2026-09-21; design + POC, nothing deployed or enabled
noetl/catalog Catalog model + API over EHDB (playbooks first) β€” no release yet (scaffold 089c607, P1 e1aae6d8) πŸ†• created 2026-10-06 β€” spec + CI 4-of-4 RED-proven + P1 model types (#427)

Architecture at a glance

Today β€” kind cluster is fully Rust

The 2026-06-04 session retired all Python deployments + their services + configmaps from the kind cluster (per the user directive "delete all legacy stuff"). Local validation runs against the Rust topology by default:

              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  Gateway         β”‚  noetl/gateway  v3.2.0
              β”‚  (Rust)          β”‚  auth Β· SSE Β· subscriptions Β· shard routing
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚ HTTPS
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  noetl-server-rust  (v2.19.7)         β”‚
              β”‚  catalog Β· execute Β· events Β·         β”‚
              β”‚  /api/internal Β· DbPoolMap sharding   β”‚
              β”‚  orchestrator engine Β· SSE            β”‚
              β”‚  workbook resolution Β· pipeline parse β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚   NATS JetStream β”‚  NOETL_COMMANDS stream
              β”‚   + Postgres     β”‚  noetl.event + noetl.command
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β–Ό                             β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ noetl-worker-    β”‚         β”‚ worker-system-   β”‚
  β”‚ rust v5.11.3     β”‚         β”‚ pool             β”‚
  β”‚ (shared pool)    β”‚         β”‚ (Rust, runs      β”‚
  β”‚                  β”‚         β”‚  system          β”‚
  β”‚ noetl-tools      β”‚         β”‚  playbooks)      β”‚
  β”‚ v2.18.1:         β”‚         β”‚                  β”‚
  β”‚ python Β· shell Β· β”‚         β”‚ Consumer:        β”‚
  β”‚ http Β· postgres  β”‚         β”‚  ..._pool_system β”‚
  β”‚ duckdb Β· rhai Β·  β”‚         β”‚ Filter:          β”‚
  β”‚ task_sequence Β·  β”‚         β”‚  noetl.commands. β”‚
  β”‚ playbook Β· noop  β”‚         β”‚  system.>        β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   Consumer:                     [Outbox publisher +
    ..._pool_shared               projector migrated
   Filter:                        to system playbooks
    noetl.commands.               via Phase 2.a]
    shared.>

Retired this session (2026-06-04): noetl-server (Python deploy), noetl-worker (Python deploy), noetl-outbox-publisher (Python deploy), noetl-projector (Python statefulset), and the noetl/noetl-ext/noetl-projector/noetl-worker-metrics services + 4 legacy configmaps + noetl-worker SA. The kind cluster is the regression-test topology for Rust-only e2e.

[Status legend: βœ… = shipped + kind-validated]

v10 playbook compatibility on Rust (closed loop)

Six interlocking gaps fixed in one iteration brought control_flow_workbook end-to-end on the Rust-only stack β€” exercising the complete v10 control-flow surface:

playbook YAML  ──►  noetl-server-rust orchestrator                 noetl-worker-rust
                                                                   + noetl-tools v2.18
─────────────────  ──────────────────────────────────────────────  ──────────────────
workload: {...}   β–Ί #56 workload + input alias decode               PythonTool wrapper
tool.kind: python β–Ί (existing dispatch)                             globals().update(args)
tool.kind:                                                          β–Ί #17 capture
  workbook        β–Ί #59 parser substitutes inline action            `result = {...}`
                                                                    global β†’ data
tool: [{...}]     β–Ί #57 ToolDefinition::Pipeline accepts both       β–Ί #18 TaskSequenceTool
  (pipeline)        flat (name-as-field) + nested (label-as-key)     runtime
                                                                    
{{ step.field }}  β–Ί #60 build_context exposes step data at top
  in next.arcs      level (not just steps.<name>); apply_event
                    captures call.done before command.completed
                    overwrites
                    
command.failed    β–Ί #58 trigger_orchestrator on command.failed
                    + dedicated short-circuit in process_in_progress
                    emits playbook.failed terminal
                    
worker β†’ server   β–Ί #55 EventEmitRequest accepts i64 wire shape
  event emission    (was rejecting integer execution_id)

Validated end-to-end:

noetl exec tests/fixtures/playbooks/control_flow_workbook
β†’ playbook_started
β†’ start (python)
→ eval_flag (workbook→python; is_hot=true captured via marker)
β†’ hot_path (next.arc when="{{ eval_flag.is_hot == true }}" matched)
β†’ parallel hot_task_a + hot_task_b
β†’ playbook.completed βœ…

Long-term Python trajectory

Per the Rust-only direction, Python pieces stay only as:

  1. Container payloads β€” runtime stays Rust; user code that wants Python ships in a container dispatched by the container tool kind (#43, in design).
  2. Back-compat GKE deployments β€” existing Python pods on the production cluster aren't removed (yet) since GKE traffic still uses them; no new feature work goes there.

The kind cluster is the canary for the Rust-only topology. When the operator runs the validation rigs end-to-end against their live cluster, R5 cutover decision will move the production topology to match.

Sessions log

Chronological notes on what each session accomplished β€” see Sessions Log.

Most recent (top of log):

  • 2026-10-08 β€” ⭐ The registry became discoverable, and five measurement gaps closed. ehdb #385 (P2–P5: discover(kind), watch_since, ephemeral units, SecretRef), #386, #387. Memory is O(parts) not O(records); ⚠⚠ a shared sequencer minted before the append lock reorders 34.2% of appends losing nothing (#362's mechanism in 20 ms); ⚠⚠ a part with 1 of 2 copies is reported durable and done; 530x for batching, and the #298 cap makes a large drain quadratic. Benches now run in CI. 1,188 tests green. β†’ full entry
  • 2026-10-02 (later) β€” πŸ”’ Closing out the CI arc β€” the gate had to be fixed twice, once by itself. ⚠⚠ CHECK 3 forbade correcting a CHECK 2 violation: moving a pointer off a side branch onto the tracked branch is never a fast-forward, so the gate reported a defect and refused the fix (#379). Private submodules removed β†’ coverage 31 of 31, zero unverifiable (#377); #372 CLOSED; clippy now gates on server + worker β€” measuring first explained the || true (#374, sweep #378); docs + doctor PR gates merged. β†’ full entry
  • 2026-10-02 β€” πŸ”’ ai-meta had NO CI at all; it now has a gate proven to bite β€” and the gate's first sweep caught the gate. #371 + #373 merged. Two of three first-sweep findings were its OWN false alarms: it cried "dangling, unpushed, or wrong SHA" at two pointers whose repos are simply private. Coverage on main is 31 of 33, now printed. Real finding: #372. Fleet audit β€” ⚠⚠ clippy is || true on 5 Rust repos, so it cannot fail (#374); 6 repos with no PR trigger and wikis that cannot host Actions (#375). β†’ full entry
  • 2026-09-30 (later) β€” βš™οΈ #362: the chain is ordered by following LINKS, not by event_id β€” a snowflake minted before insert means an event can commit into the MIDDLE of an id-ordered read. Prod armed on server v3.118.0 + ehdb v0.4.5. β†’ full entry
  • 2026-09-30 β€” ↩️ The chain-store ramp was ROLLED BACK: a third mechanism, and my verification window was too short β€” size a prod ramp by DURATION, not denominator. β†’ full entry
  • 2026-09-04 β€” ⭐ The tier deficit is 318 events, not the ~12k being quoted. Structural mirror lag measured at 0, surplus 0, 52/100 executions affected, 303 inside the #320 window. The ~12k was failed batch deliveries, not a completeness measurement. Repair endpoint (server#399, v3.101.0) merged inert β€” not run against prod. β†’ full entry
  • 2026-09-03 β€” ⭐ The p95 tail was the event-log mirror, not the publish retry β€” and my published diagnosis was wrong. server v3.100.4 live; inline fallback 24.6% β†’ 8.3%. ⚠ the tail is not collapsed and the bistability is not gone. β†’ full entry
  • 2026-09-01 β€” πŸ”₯ An 8-hour silent dispatch outage: the cmdbus manifest grew quadratically and filled the PVC while the writer reported Ready with zero logs. Resolved on prod via worker v5.129.1 β€” snapshots 172β†’32, manifest 871β†’169 MB. β†’ full entry
  • 2026-08-31 β€” 🐍 The Python platform is gone; #307's real blocker turns out to be a mirror gap. β†’ full entry
  • 2026-08-29 β€” πŸ“ #307 β€” the projection fold stops being unmeasurable; prod live in verify (server v3.92.0). The catalog gets a log, a relation, and full coverage. β†’ full entry
  • 2026-08-27/28 β€” πŸ” Secret Manager is the sole source. All four secretKeyRefs removed (server v3.90.0 / worker v5.123.0), each removal digest-checked against the file replacing it first, so the change was plumbing rather than content. Then the #267 sweep: the gateway manifest would have caused a login outage on apply, ServiceAccount/noetl-server-rust was declared nowhere, and the watchdog script existed only in the cluster. ⚠⚠ #307 projection recovery has ~zero coverage; ⚠⚠ #306 the DR apply would escalate the worker pool. Session details
  • 2026-08-11 β€” ⏱️ A cooldown that gated belief instead of retry. EHDB tier is_reachable() read its own cached-negative deadline as a verdict, so five seconds after a tier service died β€” with nothing having contacted it β€” the tier decided it was authoritative again. Belief now lives in a flag only a successful op sets. worker v5.115.3, inert, not deployed.
  • 2026-08-05 (part 9) β€” πŸ“Š Observability arc closed. P1+P2 blind spots shut across server, worker and gateway; 38 alert rules proposed (none applied). Two new defects filed: an unbacked-off claim spin (#244) and amd64-only gateway images (#245). Session details
  • 2026-08-05 (part 8) β€” πŸ”Ž 149 warn/error sites with no metric, triaged by consequence. P1+P2 closed; P3 (82) deliberately left as logs. The materializer's only data-losing outcome had no counter. Session details
  • 2026-08-05 (part 7) β€” πŸ“Š Absence is the default state of a metric. The prod gateway's /metrics served zero bytes; no binary carried a version; *_build_info now ships in all three. #242 fully dispositioned β€” 4 dead recorders β†’ 1 deliberate hold. Session details
  • 2026-08-03 β€” πŸ›‘οΈ P0 prod discovery ran (read-only), and the EHDB-only IaC turned out to be wrong about prod six ways. state_builder defaulted to server while prod runs offserver on the server and both system pools β€” a converge would have turned a running subsystem off; the three claim_* PVC names matched no PVC in prod; a wrong user_pool_container is rejected, not ignored, so the converge would have aborted half-applied; the gateway lives in ns gateway, not noetl; and the writer's memory limit would have gone 4Gi β†’ 2Gi. All six are now defaults carrying prod's measured value (ops#245 @ 5e28543, not merged). Two premises were inverted β€” prod runs all three materializer groups, so narrowing verify_groups would have failed on a live one. A seventh defect appeared only by running the adoption path on a kind rig rebuilt into prod's shape: kubectl apply's three-way merge keeps the old app: Service selector, so the writer Service resolves to zero endpoints behind a 1/1 Running pod with all nine faces up β€” a total silent outage cutting off server, both pools, gateway and KEDA. Kind: plan β†’ converge (VERDICT: PASS, PVCs adopted verbatim) β†’ clean idempotent no-op. ⚠️ Prod NOT converged; P1–P5 unrun. Session details
  • 2026-08-02 β€” πŸ§ͺ First sustained-concurrency soak of the EHDB-only platform (local kind): PASS on every paired-evidence gate β€” 360 execs, published 6840 == projected 6840 == all three group cursors, lag 0, 0 dup / 0 publish errors / cursor_errors=0 / out_of_order_appends=0; 3 SSE subscribers saw an identical 7864 frames each; KV 880 ops / 0 mismatches. Three fixes in worker#211 (open, no release cut): H5 fail-loud bus source (#221), the #209 graceful seal (tip 7781 / lag 570 β†’ reopened 7781, all 570 survived), face supervision. ⚠️ The hard-kill number could not be measured in kind β€” masked by consumption lag, so prod is more exposed, not less. ⚠️ The bus was never the bottleneck (shard lag 0 while the 8-slot pool saturated) β†’ throughput/latency remain prod-only. Defects: ehdb#311 one bad connection silently kills the WAL face; NOETL_STATE_BUILDER=shadow silently means Off; no EHDB-only IaC exists at all. ops#241 reviewed, not merged (superseded).
  • 2026-07-31 β€” 🟒 The EHDB-lag autoscaler is LIVE on prod β€” the T5-readiness sequence executed end to end; NATS untouched, T5 still the human's call. Merged + deployed: ehdb#303 (ehdb_feed_subject_lag{subject}, with the label set seeded at startup so a drained pool reports 0 rather than vanishing β€” a vanished series is a KEDA scaler error, not a scale-to-min) + ehdb#304 β†’ ehdb e457249; server#291 publish retry 0.5 s β†’ 10 s + #292 β†’ v3.58.4; worker#197 + #198 (ehdb_l0_out_of_order_appends, closes #206) β†’ v5.82.0 β†’ v5.83.0; ops#242/#243/#244. Merging #303 and #304 surfaced a silent-failure seam: the pre-existing serve_metrics_with_resume renders lag through render_prometheus, which predates #303 and omits the per-subject family β€” a writer binding it would have served the resume gauges while dropping the autoscaler's trigger series, with nothing erroring; fixed with serve_snapshot_metrics_with_resume + a test on each side. Autoscaling proven on prod, on the per-pool metric: the user pool β€” which had had no autoscaling of any kind since 2026-07-26, because KEDA 2.15 deletes the HPA while paused is set β€” went 2 β†’ 4 β†’ 8 β†’ 16 β†’ 20 under 200 real executions and back to 2 on drain, every rescale attributed to s0-metric-api-ehdb_feed_subject_lag{subject="commands-shared-shard-0"} by name; 200/200 COMPLETED, 0 dup, 0 publish errors. The split earned its keep on the first scrape β€” shard lag 4 was entirely the system pool's (shared=0, system=2). #208 re-verified live under load: writer restarted mid-burst β†’ 60/60 COMPLETED, 0 publish errors, claimers redialing unaided, the restart legible from one scrape (origin=persisted, clamped=false, replay_records=67 = the unacked tail, not a from-zero replay β€” precisely the distinction #304 was built to make). Soak 6 Γ— 40, out_of_order_appends=0 throughout. ⚠️ Two corrections the plan had wrong. (1) The nats-jetstream trigger was not a harmless no-op: noetl_worker_rust_shared still holds the 149 messages frozen at the T4 cutover, KEDA takes MAX across triggers, and it drove the pool to 15 replicas within 40 s of unpausing (every one of those rescales attributed to the NATS metric). Removed in ops#244. At the scaler interface a stale queue is indistinguishable from a busy one β€” the same failure the per-subject split exists to prevent. (2) T5 would break the SPA today. NOETL_COMMANDS_RUST is genuinely idle (last message 2d23h) but noetl_events had traffic 3 minutes before the audit, with four live consumers β€” three worker materializers plus the gateway's SSE subscriptions. That is T3, still unchecked; the gateway fails soft but loses live SPA updates and async callbacks. Inventory: playbooks/210-t5-readiness/pre-t5-checklist.md. Images reached AR by crane copy from GHCR β€” Cloud Build's publish-ar fails on every release (build SA cannot stage source in the _cloudbuild bucket) β†’ #211, needs a human IAM grant. ai-meta gitlinks ehdbβ†’e457249, serverβ†’91a4a66, workerβ†’4c83117, opsβ†’6469482.
  • 2026-07-30 (docs sweep) β€” 🧭 Issue + wiki reconciliation after the prod deploy. #205 and #208 closed prod-validated; #210 filed for the user pool having no autoscaling since 2026-07-26; #209 annotated with what the seal race actually cost in prod (a latency artefact, not loss β€” the crash path is the real exposure); #194 given a single consolidated T5 status board; #204–#210 added to the roadmap board. Dashboard, ecosystem map, releases and the ehdb / ops / server / worker wikis brought to server v3.58.3 / worker v5.81.3, and the VictoriaMetrics β†’ Google Managed Prometheus correction applied. No code, no prod action.
  • 2026-07-30 (later) β€” πŸ“ˆ The last T5 prerequisite β€” an EHDB-lag KEDA autoscaler for the user pool, so deleting NATS cannot strand it; plus the two #205/#208 prod-rollout cleanups. 4 PRs held (ops#242, ehdb#303, ehdb#304, server#291). Corrected two assumptions β€” prod monitoring is GMP, not VictoriaMetrics (no in-cluster PromQL endpoint β†’ KEDA metrics-api scraping the writer :9102 directly, no new IAM), and the exposed lag is whole-shard, not per-pool (a stuck system-pool command would pin the user pool at max β†’ ehdb_feed_subject_lag). Scale-up 3 β†’ 6 β†’ 12 β†’ 20 proven in kind on the real metric, then back to min on drain; prod applied paused, unpause staged. T5 HELD.
  • 2026-07-30 (prod deploy) β€” 🏁 The #301+#302 bundle lands on shastaratech prod: EHDB is now ~2.4Γ— faster than NATS, and the bus survives a writer restart β€” and it turns out it had not been surviving one for 2.4 days. server v3.58.3 / worker v5.81.3 / ehdb 86a24f9 in one rollout. Dispatch p50 138.5 / p95 156.4 / p99 181.0 ms (n=60, unsaturated) vs the 338 ms NATS baseline (#205 CLOSED β€” T5 latency gate GO). Writer pod deleted mid-load β†’ 38/38 COMPLETED, 114 = 38Γ—3 commands, 0 dup / 0 loss, ~2.7 s redial, cursor-resume not replay (#208 CLOSED). ⚠️ Found on arrival β€” user-playbook dispatch had been silently dead since 2026-07-28 07:49Z (~2.4 days): the writer restarted, every user-pool claimer wedged on a half-open socket, and noetl-worker-system-pool-shard1 restarting later kept system commands flowing and masked it completely. The T4 sign-off had not survived its first writer restart; this deploy remediated it. Two rough edges left open: the resume log line cannot be told apart from a replay-from-0 (ehdb#304) and 2 Γ— HTTP 500 during the writer gap (server#291). NATS untouched; T5 HELD.*
  • 2026-07-30 β€” πŸ” EHDB command bus survives a writer restart β€” ai-meta#208 both defects fixed + kind-validated, and a third found; 3 PRs held for review. Branched off ehdb main at 03e94be (#301). Defect 1 (a writer-only restart wedged dispatch silently β€” claim_next parks on a half-open socket, so the redial path hangs off an Err that never comes): TCP keepalive on every ehdb-feed socket (~11 s detection even with no FIN/RST) + a negotiated coordinator heartbeat (one beat up front arms a 3-missed-beat read deadline for the connection; catches a writer that is alive but stuck, which keepalive cannot) β€” heartbeats only go to a client that asked, so the wire stays backward compatible. Defect 2 (a restarted writer replayed its whole shard from cursor 0 β€” kind lag 2738 draining at ~1/s): CursorStore persists committed_cursor() atomically on the writer's volume + ClaimCoordinator::resume starts there (NOETL_COMMAND_BUS_CURSOR_PERSIST_MS, NOETL_COMMAND_BUS_CURSOR_FALLBACK=tail|beginning), the cursor is clamped to the reopened log's tip (a log that lost an unsealed part reopens behind the cursor β†’ without the clamp the bus goes permanently dark), and the writer seals its log on SIGTERM (the engine reopens from its durable manifest, so an unsealed part is invisible after a restart). ehdb_feed_shard_committed{shard} added to the writer :9102 β€” lag reads 0 both when caught up and when a replay just finished, so the cursor is the series to watch. Third defect, found while validating: the server dropped one command per writer restart (EHDB publish failed: early eof + HTTP 500; command.issued durable, nothing on the bus) β†’ redial-and-retry, 3 attempts 250 ms apart. Kind before/after, same cluster: pre-fix a writer-only rollout restart gave 21 issued / 0 claimed with lag frozen at 3094 for 100 s; post-fix all 3 claimers redialed in ~2 s, the writer resumed from_cursor=3246 origin="persisted" at lag 0, and a writer restarted mid-burst left 30/30 executions completed, 0 dupes, lag β†’ 0. No-regression re-proved: pod-kill redelivery 39/39, pool isolation 0 cross-pool claims (84 + 83 across the two #166 system shards), 0 spurious redials on a 10-min idle bus. ehdb#302 + worker#196 (on #195) + server#290 (on #289) β€” all held for review; prod bundles #301 + #208 in one deploy. Kind restored to NOETL_COMMAND_BUS=nats; nothing in prod. Follow-up: the SIGTERM seal races in-flight ingest (one command per restart recovered by the orchestrator re-issue ~30 s later β€” a latency artifact, not loss).
  • 2026-07-28 β€” πŸš€ EHDB is the production command bus β€” L1 T4 full flip SUCCEEDED (2026-07-27) Β· πŸ› #203 silent feed-delivery loss fixed Β· 🐍 Python cleanup. The staged Option-2 (single command shard) cutover completed on shastaratech prod with server v3.58.1 / worker v5.81.1: shadow 0 divergence β†’ canary 30/30 (0 system claims, pod-kill redelivery 12/12) β†’ full flip 30/30 + 50/50 soak = 80/80, 0 dup, 0 loss, server NATS_pub=0, feed lagβ†’0, #166 state sharding intact. NATS stays installed as the one-command rollback; T5 (delete NATS) held on the human. ⚠️ Dispatch latency p50 285–557 ms vs ~200 ms NATS (~1.5–2.8Γ—, sub-second) is the T5 go/no-go item β†’ #205; out_of_order_appends not on writer :9102 β†’ #206. The flip needed a real bug fixed first: producer-assigned sparse snowflake ids arriving out of order past the follower cursor were filtered out of the feed forever β€” silently, lag=0 (~10% loss); fix = the writer assigns the ordering key (ehdb#300 β†’ ehdb d4b6235), kind 17/40 β†’ 41/41, then prod-validated β†’ #203 CLOSED; #202 CLOSED (Option 2 executed; the per-shard user-pool split was never needed). Releases: worker v5.79.0/v5.80.0/v5.81.0/v5.81.1, server v3.58.1. Python: 5.0.0 on PyPI, shims removed (noetl#693), legacy PyPI landmine retired (noetl#694 β€” pypi.yml/pypi-v* is now the sole publisher); the big server/worker/outbox retirement is BLOCKED on noetl/core's imports β†’ #207. Infra: Cloud Build β†’ Artifact Registry (#204, worker#194 + server#288 OPEN, amd64-only by design). ai-meta gitlink bumps + RFC write-ups left PENDING (dirty shared tree); wikis + issues updated directly.*
  • 2026-07-26 β€” 🚚 Prod migrated to the shastaratech org + cut over Β· 🐍 noetl 5.0.0 on PyPI Β· πŸ”€ EHDB L1 T4 executed on prod (held at canary). Prod stack moved to GKE Autopilot noetl-prod-autopilot (us-central1, shastaratech-noetl-prod) off noetl-demo-19700101; billing unblocked (Free-Trialβ†’paid + noetl-devβ†’noetl-prod slot swap); images/IAM-WI/10 secrets/NOETL_ENCRYPTION_KEY(19/19 decrypt)/DBs migrated; cloudflared connector flip = 25/25 external /health 200 zero-blip, old prod left warm (shastaratech/gcp-org-playbooks#13, billing #9). noetl 5.0.0 LIVE on PyPI β€” Rust CLI via PyO3 (abi3-py39), 4 artifacts (macOS arm64 + Linux x86_64/aarch64 + sdist; Windows/Intel-Mac dropped); cli v4.20.0 (lib extraction) + v4.21.0 (DuckDB feature-gate); Stage-4 retirement evidence-only (#201). EHDB L1 T4 command-bus cutover on prod: Step 0 + Shadow PASS (0 divergence, 121β†’NATS==121β†’EHDB, 2 shards), HELD at canary on the per-shard user-pool blocker β†’ bus back on NATS, prod clean; server v3.58.0 / worker v5.81.0 + both writers deployed (inert); two paths in #202; IaC playbooks/194-l1-t4-prod-iac/ (#194). Write-behind sink slices A/B/C merged default-off (worker#190/#191/#192, #199); release-red #200 CLOSED. ai-meta gitlink pointer bumps + RFC write-ups left PENDING (busy shared tree; wikis pushed directly).
  • 2026-07-14 β€” 🧩 Provider tool round 5 β€” noetl provider {plan,drift,orphans,adopt} CLI verbs + query-tier fetch adapter + stack-scoped ownership, merged + released. tools#90 β†’ noetl-tools v3.25.0 (5a06c7e): extract_facts_from_tier_records/fold_tier_records (wire-shape eventlog-tier fold β€” unwrap the payload string + recursive fact search; fixes the round-3/4 fixed-path bug β€” extract_facts_from_eventlog never matched the real context.result.context.data.provider_fact nesting, so a fold over real data on 3.23/3.24 silently returned nothing) + OwnershipModel::for_stack/detect_orphans_scoped/detect_stack_conflicts. cli#68 β†’ noetl-cli v4.17.0 (a2e5b4d): the noetl provider verbs (Terraform-style CLI over the tool, local mode; glue over ProviderTool::execute+provider_state+to_tools_config); re-pin noetl-tools 3.24β†’3.25; noetl-executor unchanged at 0.7 (zero executor/ files touched β€” version-bump discipline held). crates.io tarball-verified + released-binary proof (noetl-v4.17.0-darwin-arm64, all 4 verbs GET-only zero-writes against the mock; drift over HTTP --server; adopt dry-runβ†’digestβ†’applyβ†’stale-refusal). Follow-up 3 (first-class event type) DEFERRED (projection column rewrites the secret-free invariant tests + rebuilds server/worker). ai-meta gitlinks toolsβ†’5a06c7e + cliβ†’a2e5b4d. Umbrella #189 OPEN. No prod/kind/live-cloud β€” mocks + unit tests only.
  • 2026-07-13 β€” 🧩 Provider tool round 4 β€” drift adoption (reconcile policy report/enforce/adopt + confirm-gated adopt), merged + released. tools#89 β†’ noetl-tools v3.24.0 (c172efb); cli#66 β†’ noetl-cli v4.16.0 + noetl-executor 0.7.0 (7557dde). report new-default (converge needs reconcile:enforce); adopt accepts live actual as new desired, read/GET-only, confirm-gated like destroy (blind+stale REFUSED). All three crates crates.io-verified. ai-meta gitlinks toolsβ†’c172efb + cliβ†’7557dde. Umbrella #189 OPEN.
  • 2026-07-13 β€” πŸš€ Provider tool round 3 β€” confirm-gated destroy + EHDB ownership projection + endpoint override, merged + released. tools#88 β†’ noetl-tools v3.23.0 (aa32938): destroy verbs (*.delete/services.disable/*.iam.remove_binding) gated by a live-resolved SHA-256 plan_digest (dry-run emits plan + digest; apply needs confirm:<digest>; blind apply + stale digest REFUSED, nothing deleted; no wildcard/bulk; needs auth: even to plan; real destroys human-run); secret-free provider_fact per converge + pure provider_state fold (fold_facts/compute_drift/detect_orphans) over the raw eventlog tier (not the secret-free /api/ehdb/events read-model β€” #178 collision resolved); config-level endpoint: override for offline mock/emulator runs. cli#65 β†’ noetl-cli v4.15.0 (94cc72a): local-mode endpoint/stack/confirm passthrough + re-pin to noetl-tools 3.23.0 (no fresh libduckdb-sys compile). Local-mode PROVEN on 3.23.0 (mock, no live cloud): endpoint-override apply β†’ no-op; destroy dry-run β†’ plan + plan_digest 6b5efdbb…, no DELETE. 39 provider tests + 464 lib + clippy clean. ai-meta gitlinks toolsβ†’aa32938 (f6eeb479) + cliβ†’94cc72a (cbc7be99). Umbrella #189 OPEN β€” round-4 = drift adoption. No prod/kind/live-cloud, no real destroys β€” mocks + unit tests only.
  • 2026-07-13 β€” πŸš€ Provider tool round 2 β€” apply-mode converge + bounded LRO + local-mode CLI dispatch, merged + released. tools#87 β†’ noetl-tools v3.22.0 (eb3d8c7): idempotent GET-first converge for the 4 ensure/ensure_binding actions (+ services.enable/projects.link, re-run = no-op) + bounded inline LRO polling (descriptive timeout + documented callback overflow). cli#64 β†’ noetl-cli v4.14.0 (2d46013): kind: provider dispatches through noetl exec --runtime local (same tool the worker runs), re-pinned to noetl-tools 3.22.0 (libduckdb-sys unchanged β†’ no fresh compile), supersedes cli#63 (closed). Local-mode PROVEN (dry-run would_call + apply-mode converge entry; mocks not live cloud). ai-meta gitlinks toolsβ†’eb3d8c7 (6cd1ba38) + cliβ†’2d46013 (32a8fb86). Both design forks DECIDED for round 3 (EHDB-backed state; confirm-gated destroy). Umbrella #189 OPEN. No prod/kind/live-cloud β€” mocks + unit tests only.
  • 2026-07-13 β€” πŸ†• NoETL cloud provider tool (kind: provider) β€” Google REST MVP designed β†’ implemented β†’ merged + released noetl-tools v3.21.0 (tools#86 5e427fb, release cd9d71c); ai-meta pointer e9b3e14e; umbrella #189 OPEN (round-1). No prod/kind/GCP β€” impl + unit tests only.
  • 2026-07-10 β€” πŸ—‘οΈ EHDB segment-GC limits-based retention β€” a shadow store can self-bound without a consumer. Merged noetl/ehdb#271 β†’ ehdb main 49db503 (engine) + noetl/worker#178 (knob) + noetl/worker#179 β†’ worker main 521fd10 (re-pin ehdb to merged #271; #178 pinned the pre-merge branch commit). Keep-last-N (count-based) retention reclaims independent of consumer interest; plan_reclaim composes it as min(retention_target, interest_ceiling) β€” never reclaims ahead of a real consumer, drives when there is none, min_retained_segments is the hard floor; the only engine change is inside plan_reclaim (watermark-first commit / shared-tier coherence / per-shard lock all inherit unchanged). Keep-last-N not max-age (mtime resets on hydrate/cold-load). Config NOETL_EHDB_EVENTLOG_GC_MAX_RETAINED_SEGMENTS + worker NOETL_EHDB_EVENTLOG_SEGMENT_MAX_BYTES. 251 tests (+3 retention: self-bounds a consumer-less store / never reclaims ahead of a lagging consumer / respects the min floor); clippy+fmt clean; bench variant. Organic in-kind soak PROVEN: live store 19β†’2 segments / 2,147 B (from ~22 MB) under retention with no consumer, held flat; reclaimed=6, 0 restarts, replay intact. LOCAL kind only β€” no GKE/prod; noetl.event never purged. ai-meta gitlink bump repos/ehdbβ†’49db503 + repos/workerβ†’521fd10.
  • 2026-07-10 β€” πŸ—‘οΈ EHDB segment-GC worker wiring + in-kind soak β€” D11 FULLY CLOSED (mechanism + deployed exercise). Merged noetl/worker#177 β†’ worker main b25263f: a periodic per-owned-shard eventlog_gc task (local + shared reclaim_shard) on spawn_blocking, gated off on every axis; a process-global per-shard advisory lock serializes GC against the per-op append path (resolves the two-writers-per-shard fork + closes a latent append↔append race). noetl_ehdb_eventlog_gc_* metrics; ehdb-selfcheck durable-eventlog-gc [--drive 1]. In-kind soak (v5.72.3-ehdbgc254, user pool, /ehdb-durable PVC, GC + interval=30s): periodic task spawned + healthy (eventlog_gc_ops_total{outcome="noop"}, last_ok=1 β€” correct noop, shadow store has no consumer); --drive 1 on the real PVC reclaimed shared 40β†’10 at watermark 30 (gc_holds, replay/cold-load/new-owner-hydrate coherent); 0 restarts; user+system pools uniform. Fork surfaced: the deployed shadow event-log has no durable consumer β†’ interest-based GC reclaims nothing organically (correct); a limits-based retention mode (keep-last-N/max-age) for shadow, or a primary-consumer, is the one remaining optional follow-up. LOCAL kind only β€” no GKE/prod; noetl.event never purged. ehdb-wiki d68b988; ai-meta gitlink bump repos/workerβ†’b25263f.
  • 2026-07-09 β€” πŸ—‘οΈ EHDB shared-tier segment GC β€” coherent reclamation across the shared medium (D11 done for both topologies). Merged noetl/ehdb#270 β†’ ehdb main adca378: a monotonic per-shard cross-replica reclaim watermark on the shared medium; SharedTierEventLog::reclaim_shard reclaims local + shared watermark-first (reconcile β†’ plan β†’ commit watermark β†’ reclaim local β†’ delete shared), so cold_load/hydrate_owned_shard skip <= watermark and never re-pull a reclaimed segment (no un-reclaim on transfer); reconcile_owned_shard realigns a crashed owner. Store refactored into plan_reclaim (decision) + apply_reclaim/reclaim_to_segment (mutation). 248 tests (+5 shared-GC incl. crash-window reconcile); CLI durable-eventlog-shared-gc pruned shared 40β†’10 objects at watermark 30, cold-load + new-owner hydrate coherent; bench ~25 ms@50 / ~38 ms@150. Off by default + not auto-invoked. D11: GAP β†’ delivered + drive-tested for BOTH single-writer local (PVC) AND shared-medium β€” remaining is operational (worker wiring + in-cluster soak). LOCAL/engine only β€” no GKE/prod; noetl.event never purged. ehdb-wiki 94afe29; ehdb#254 comment; ai-meta gitlink bump repos/ehdbβ†’adca378 + repos/ehdb-wikiβ†’94afe29.
  • 2026-07-09 β€” πŸ—‘οΈ EHDB durable event-log segment GC β€” the R1 / D11 gap (Stage-C blocker) closed for single-writer local. Merged noetl/ehdb#269 β†’ ehdb main 93f3060: interest-based (consumer-ack watermark) reclamation of consumed sealed segments behind NOETL_EHDB_EVENTLOG_GC=consumer_ack (off by default). Base-offset (retained gapless-from-reclaimed+1; reclaimed_through==0 β‡’ byte-identical to pre-GC), durable fsync'd reclaim.json manifest = crash-atomic commit point (crash mid-GC re-deletes below-watermark leftovers on next open), write-forward of consumer Ack frames before unlink. Fixed tail() pending to the absolute tip. 242 tests (+10 GC incl. crash-mid-GC-via-manifest), clippy/fmt clean; CLI drive 41β†’11 segments at watermark 30. D11: GAP β†’ delivered + proven for single-writer local (PVC), unblocking the Stage-C durable-primary consideration for that topology; shared-tier object reclamation is the one remaining follow-up (started this session). LOCAL/engine only β€” no GKE/prod; noetl.event never purged. ehdb-wiki e830957; ehdb#254 comment; ai-meta gitlink bump repos/ehdbβ†’93f3060 + repos/ehdb-wikiβ†’e830957.
  • 2026-07-04 β€” πŸ” #169 Auth0 JWT signature verification β€” SHADOW canary LIVE on prod (server v3.50.0β†’v3.52.0, single replica, NOETL_AUTH_VERIFY_SIGNATURE=shadow); focused finish handover Handover-169-JWT published (docs/state-sync only β€” no code/prod/flag change).
  • 2026-06-29 β€” 🎟️ #164 Muno ACTIVITIES render fixed on prod β€” planner v59 (cid 660442656010404734, rollback v58 660417115383399292). The activities turn rendered blank (render: null) though the provider returned 10 real activities: render_widget_chat crossed the ~100 KB result-tier offload threshold β†’ noetl:// ref that 404s on prod (#154). The 10 activity cards were duplicated ~6Γ— (widgets/first_widget/final_slot_state/post_docs/two post_events), each ~1.2 KB dominated by a raw [:600] HTML description (75 KB render on a fresh thread). Fix travel#88: _activity_card strips HTML β†’ short plain-text (160 ch) + modalities 5β†’2 β†’ card ~0.57 KB, render 75.3β†’39.5 KB. No widget-schema/FE change (CLOSED activity_list.schema.json still validates). Prod-verified: real activity_list (10 activities, no synthetic, no offload, schema PASS); places/hotels/flights/login unaffected; 0 restarts. #164 open until travel#88 merges + bump; refs #155.
  • 2026-06-29 β€” 🏨 #162 Muno HOTELS bookable end-to-end on prod β€” planner v58 (cid 660417115383399292) + HotelBeds MCP v4 (cid 660409660586853242, book_hotel checkRateβ†’bookings, TEST/SANDBOX-guarded). Verified: real TEST booking refs 197-14375473/74/76 (CONFIRMED, env=test) β†’ hotel_confirmation; search + Trip-to-Paris + login healthy; 0 restarts. Rollback = planner v57 cid 660221263901623160. Closes #162; refs #155.
  • 2026-06-28 β€” πŸš‘ #157 Muno hotels-turn drive wedge fixed forward + DEPLOYED β€” worker v5.47.3. A hotels follow-up ("What hotels are in Paris?") wedged in RUNNING because the hotelbeds render-hop __orchestrate__ drive result (~138KB) crossed the 100KB inline budget and offloaded to a result-store ref the server can't resolve under NOETL_RESULT_STORE_DUAL_WRITE=false (#104) β€” a #154-class wedge. worker#141 (squash f8f4d12, released v5.47.3 9f83ab5) keeps the __orchestrate__ result inline regardless of size. Bumped ai-meta repos/worker 5.47.2β†’5.47.3 (60938f4), rolled both pools new-pod-first (0 restarts, materializer balanced/lag-0, Fix A NATS reconnect 0/30s intact). Prod-verified: hotels exec 329775581438681088 COMPLETES + hotel_list widget; places no-regression exec 329775627576025088 COMPLETES + place_list. Worker-behavior-only β€” no OQ5/result_store/IAM/shadow change; rollback :5.47.2. Closed #157, referenced #155.
  • 2026-06-28 β€” πŸ“± Muno mobile responsive layout SHIPPED to prod β€” the live travel SPA was still serving the broken overlapping 3-column shell on mobile because the fix had been built but never deployed. Merged travel#80 (squash β†’ a8e78f1): reflows the 3-panel shell to a single column at the md breakpoint, sidebar + Trip-state become temporary MUI drawers (hamburger ☰ + luggage 🧳 buttons). The merge triggered the repo's Cloudflare Pages workflow (run 28329391574) β†’ prod deploy https://417ab9ee.travel-bgo.pages.dev (bundle index-0naZf1z6.js). Verified live signed-in at 380px: full-width single-column chat, sidebar/Trip-state as left/right drawers with backdrop, composer full-width + Send reachable; desktop still 3-column. Minor nit: 16px composer-<form> overflow (cosmetic, follow-up). ai-meta repos/travel 79be476β†’a8e78f1; SPA + Cloudflare Pages only (no backend/OQ5/IAM/GKE).
  • 2026-06-27 β€” πŸš€ #104 result_store dual-write RETIRED on prod β€” server v3.47.0 (server#267, bd831e6, image @sha256:140066d9…) deployed behavior-neutral (flag default-on), then NOETL_RESULT_STORE_DUAL_WRITE=false flipped. Over-budget result_store writes stopped (dual_write_skipped_total climbs, put_total/dual_write_total flat); the #104 tier serves over-budget results by URN with 0 resolve errors; execs COMPLETE; sole-writer balanced 142==142==142 / dup-0 / lag-0; server + system-pool 0 restarts. Existing result_store data frozen (not deleted); revert = flagβ†’true (deeper: RESULT_MINT_AUTHORITATIVE=false; image rollback v3.46.0 @sha256:6254838…). Probe muno/oq5-retire/test/overbudget. ai-meta server pointer β†’ v3.47.0.
  • 2026-06-26 β€” 🧱 SLM platform foundation G1/G2/G3 merged + released (review-only): tools v3.19.0 (container GPU placement + poll-to-terminal helper, #82), worker v5.47.0 (watcher-free poll completion fallback, flag NOETL_CONTAINER_COMPLETION_POLL default-off, #139), server v3.46.0 (versioned model/dataset/eval registry, flag NOETL_REGISTRY_ENABLED default-off, #266); ai-meta pointer bump 2e79af6. Plus the SLM teacher swapped OpenAIβ†’Vertex Gemini (gemini-2.5-proβ†’flash fallback, Workload-Identity metadata token mint, no API key/secret) β€” ops #216 + travel #75 open on the Phase-A track. ops#214 held (stacked on the ops SLM branch). No prod deploy β€” both runtime flags ship default-off.
  • 2026-06-25 β€” 🧠 Domain-Specific SLM platform Phase A (#140) landed β€” dataset_build + eval shipped as generic, config-driven NoETL playbooks (MLOps-as-playbooks). ops PR #212 (generic automation/mlops/slm pack + lib/ engines + toy 2nd domain) + travel PR noetl/travel#73 (travel instance: deterministic oracle as zero-cost label source, 45-turn seed corpus, contract schemas). Config-only proven (RFC Β§2.2 extraction test: a support-triage toy domain runs the same playbooks zero-edits). Baseline (45 turns β†’ 29/16): 100% schema validity, eval gate passed, floor latency sub-ms, 9 intents/4 tools/10 widget types. Validated via noetl exec -r local. OpenAI ceiling deferred (teacher budget); finetune/serve gated on G1/G2/G3; model-choice deferred. Review-only, no prod change.
  • 2026-06-25 β€” 🧠 Domain-Specific SLM platform RFC + umbrella (#139) + 11 children filed (design only).
  • 2026-06-23 β€” πŸͺΆ #104 OQ5 Option A SHIPPED β€” producer-staged result tier (worker v5.46.0); decouples the over-budget tier write from result_store (flag default-off). worker#132 + e2e#81 + docs#186 merged, ai-meta pointer #128; ops#206 soak-gate rules open. result_store-retirement soak defined but NOT started (gated on a prod producer-stage flip).
  • 2026-06-23 β€” πŸš€ #104 β€” WI/ADC GCS auth MERGED (server#265, fad5d8a β†’ noetl-server v3.45.0) + ROLLED TO PROD GKE: server runs as the WI-bound KSA noetl-server-rust (@sha256:d3cbf1ad…) with the result-tier GCS ENV applied (OBJECT_STORE_BACKEND=gcs, bucket noetl-demo-19700101-results, auth=adc, cell usc1-a/256); /api/internal/cells reads it back; all tier-enable flags stay OFF β†’ tier inert. Server healthy auth=adc (no token minted β€” lazy), off-server cutover sole-writer/lag-0/never-scan (smoke + system/scheduled_cleanup COMPLETED, published==acked=13, chain clean), 0 restarts. Prod configured + WI-live, ready for staged Bβ†’Cβ†’D enablement.
  • 2026-06-23 β€” πŸͺΆ #104 Phases E + F MERGED β€” the FINAL build phases (side-effect durability barrier + result-tier GC + DR), repo-only, flags default-off β†’ inert in prod. Phase E: tools#78 β†’ noetl-tools v3.17.0 (registry::kind_is_side_effecting, members noetl-directives + noetl-locator re-published first), worker#130 barrier β†’ v5.44.0 (repointed onto published 3.17, no patch), e2e#79. Phase F: server#264 GC sweeper β†’ noetl-server v3.44.0, worker#131 DR re-derive β†’ noetl-worker v5.45.0, ops#205 + e2e#80. ai-meta pointers + deployment-spec wiki rows bumped. A–F build phases complete; #104 stays OPEN (operational items remain).
  • 2026-06-23 β€” πŸͺΆ #104 Phase D MERGED β€” the minting flip (dual-write window). All 4 PRs squash-merged in dependency order (server β†’ worker β†’ ops β†’ e2e), flags default-off β†’ inert in prod. Repo-only; prod minting cutover is a separate next task. NOETL_RESULT_MINT_AUTHORITATIVE makes the URN β†’ Feather/GCS tier the authoritative result store with noetl.result_store kept as the reversible dual-write fallback. No new crate publish β€” no Cargo.toml dep change in either PR; server resolves noetl-locator 0.1.1, worker resolves from the registry. semantic-release cut noetl-server v3.43.0 + noetl-worker v5.43.0: server#263 β†’ v3.43.0 6f6b9ef, worker#129 β†’ v5.43.0 be6863a, ops#204 b19b759, e2e#78 07e85aa. ai-meta pointers bumped. OQ5 β€” result_store retirement β€” DECIDED metric-gated (drop dual-write once mint_authoritative_total{path=legacy_fallback} holds 0 across a staging soak + retention time floor), gated on a not-yet-done byte-source re-plumbing prerequisite (materializer fetches the payload from result_store today). #104 stays OPEN (Phase E/F + minting prod-cutover remain).
  • 2026-06-23 β€” πŸͺΆ #104 Phase C MERGED β€” resolve-by-URN read path (server GCS object backend + cell registry + GET /api/internal/cells; worker resolve-by-URN + fixes B/B1, closes OQ6). All flags default-off β†’ inert in prod. Merged in dependency order (server β†’ worker β†’ e2e): server#262 β†’ noetl-server v3.42.0 c2d5ca9, worker#128 β†’ noetl-worker v5.42.0 7971041 (38 unit tests), e2e#77 39dc880 (3-pass rig + fake-gcs). No new crate publish β€” worker resolves noetl-tools 3.16.0 + adds published arrow=53; server resolves noetl-locator 0.1.1, both from the registry (no repoint). ai-meta pointers bumped; #104 stays OPEN (Phase D minting flip remains).
  • 2026-06-22 β€” πŸͺΆ #104 Phase B MERGED β€” shadow Feather result tier (5 PRs, flags default-off β†’ inert in prod). Merged in dependency order: tools#77 (β†’ noetl-tools v3.16.0 + noetl-locator 0.1.1 published; member bumped 0.1.0β†’0.1.1 pre-merge so the additive parse/from_locator API publishes) Β· server#261 (β†’ v3.41.0, sibling noetl_result_materializer consumer) Β· worker#127 (β†’ v5.41.0, the consume-loop) Β· ops#203 (flag + cell seed) Β· e2e#76 (kind rig). No downstream repoint needed (both resolve from the registry, no git/branch dep). ai-meta pointers bumped; #104 stays OPEN (C–F remain). Session details
  • 2026-06-22 β€” πŸ“¦ #104 Phase A MERGED β€” slim noetl-locator 0.1.0 extracted + published to crates.io; server accepts the canonical result URI behind NOETL_RESULT_URI_ACCEPT (default off). Repo-only, no prod deploy. Squash-merged tools#76 with a feat(locator): subject β†’ noetl-tools v3.15.0 (dc0c5d8) and the release CI published the new workspace member noetl-locator 0.1.0 (member-publish order: locator before the root crate). Repointed server#260's temporary git-dep to noetl-locator = "0.1.0", confirmed cargo build/cargo tree resolve from the registry with the heavy graph absent (duckdb/kube/arrow/tonic/rhai/gcp_auth = 0), 623 tests green; squash-merged β†’ noetl-server v3.40.0 (c89d078) + the e2e#75 kind rig (eeca8b7). Resolves the umbrella's OQ7. Flag default-off β†’ inert in prod until a later server rollout. ai-meta pointers bumped (repos/toolsβ†’dc0c5d8, repos/serverβ†’c89d078, repos/e2eβ†’eeca8b7); #104 stays OPEN.
  • 2026-06-22 β€” 🧹 CQRS / off-server program close-out + tracking reconciliation (repo-only, no prod changes). Merged e2e#74 (squash 1deadf1, the prod-scoped Rust regression validator, 28/30 against live prod) + bumped repos/e2e. Closed #103 (CQRS event log β€” prod cutover live + validated; e2e#74 was its last piece), #102 (server#198/#199 landed; worker-side superseded by #103), #101 (acceptance met + kind-validated; consume side moved to #115 Phase 1). Closed superseded Python ops PRs #189/#190/#192 (replaced by the Rust in-process worker materializer noetl-worker/src/materializer.rs). Archived the completed 2026-06-18-orchestrate-plugin-dissolution + stale 2026-06-09-rust-stack-session-snapshot handoffs. Reconciled roadmap board 3 + this Active-umbrellas table. Fixed the failing release-server CI (cargo publish β€” added a version to the noetl-orchestrate-core path dep + publish the member first, mirroring noetl/tools).
  • 2026-06-22 β€” 🐘 #95 CLOSED β€” postgres pg_value_to_json temporal/identity serialization shipped to prod end-to-end. The Rust postgres tool returned null for a tz-naive timestamp (NaiveDateTime) column plus date/time/uuid/numeric/bytea (the auth0_login expires_at: null repro). Merged tools#75 (squash 06302ac β†’ semantic-release noetl-tools v3.14.2 6d9b674, published to crates.io) + worker#126 pin bump 3.14.1β†’3.14.2 (squash 60a849d β†’ noetl-worker v5.40.5 da24952). Built the prod image via Cloud Build us-central1 (noetl-worker-rust:v5.40.5 @sha256:45212dbe…) and rolled it by digest onto prod noetl-worker-rust + noetl-worker-system-pool (server stays v3.39.5, CPU 250m/2 kept) β€” rolling restart clean (2/2 + 1/1, 0 crashloop), off-server CQRS cutover stayed healthy (materializer sole-writer lag 0, command lag 0, project_errors 0, WAL index rehydrated 5 execs/198 events per #119). ai-meta repos/toolsβ†’6d9b674 + repos/workerβ†’da24952.
  • 2026-06-22 β€” βœ… #127 CLOSED β€” the task_sequence per-sub-task context optimization merged, released, and shipped to prod; the code-opt + CPU-limit bump now compound on the live off-server cutover. Merged tools#74 (squash 9dd9aa6 β†’ semantic-release noetl-tools v3.14.1 c8656c1, published to crates.io) β€” render_value builds the proxied minijinja context ONCE + threads it through the recursion (render_value_with/render_with), and new build_context_with_overlay skips the redundant to_template_context() HashMap clone + per-block ExecutionContext clones; micro-bench βˆ’61.6%/2.6Γ— per-sub-task context cost (407 tests + 2 equivalence pins + clippy). Bumped the worker noetl-tools pin 3.14β†’3.14.1 (worker#125, squash 1a10a73 β†’ noetl-worker v5.40.4 0afbf5c; deps-only, no worker source change, clippy clean). Built the prod image via Cloud Build (us-central1-docker.pkg.dev/noetl-demo-19700101/noetl/noetl-worker-rust:0afbf5c) and rolled it onto prod noetl-worker-rust + noetl-worker-system-pool (server stays v3.39.5/.6, worker CPU limits kept req 250m/limit 2). Rolling restart clean (pods Ready, 0 crashloop), and the off-server CQRS cutover stayed healthy throughout β€” materializer sole-writer projected==acked, lag ~0, command lag 0, executions completing, system-pool rehydrated its WAL index (#119). ai-meta repos/toolsβ†’c8656c1 + repos/workerβ†’0afbf5c pointers bumped + wiki. #127 closed + board 3 β†’ Done.
  • 2026-06-22 β€” ⚑ #127 PROD WORKER CPU LIMIT RAISED 1β†’2 + APPLIED LIVE β€” the ~20% batch-throughput win materialized on prod. ops#202 (85e3c23) raised request 100mβ†’250m / limit 1β†’2 on both noetl-worker-rust + noetl-worker-system-pool; ai-meta repos/ops pointer bumped. Kind profiling found the step workers pegged one CPU and spent 38–47% throttled at the 1-CPU limit (per-sub-task running_ctx.clone() + to_template_context() hot path); 2 CPU cut a 10k batch 166–172sβ†’137.6s (~20%, zero patient loss). Live prod baseline confirmed 1 CPU (kind had 0.5); nodes 4Γ— ek-standard-16 (~63.5 CPU) at ~2% β†’ headroom. Applied via kubectl set resources: rolling restart clean (all pods Ready, 0 restarts), off-server CQRS cutover stayed healthy (materializer sole-writer lag 0, WAL index rehydrated). PROD gate vars / image / DB untouched; the deeper context-clone hot path is the structural follow-up. #127 stays OPEN.
  • 2026-06-22 β€” βœ… #123 SHIPPED + CLOSED β€” a non-iterable loop in: no longer silently wedges (commands=0, RUNNING forever); the off-server drive now decodes the {"error":…} envelope and emits a terminal playbook.failed (server v3.39.6). server#258 (squash 275b914 β†’ v3.39.6 7f109a9): apply_worker_orchestration decodes the drive ERROR envelope (decode_orchestrate_error) and emits the terminal failure (metric noetl_orchestrate_drive_total{stage="drive_error"}, structured execution_id), matching the in-process drive; orchestrate-core prefixes the offending step name onto the existing evaluate_loop error. An empty iterable ([]/{}) still short-circuits to next. 600 server + 135 orchestrate-core tests + clippy clean; kind-validated prod-exact (absent workload.batch_slots β†’ FAILED with a clear message, valid [1,2,3] still COMPLETED 3-way, #120/#124 unaffected, 0 restarts). Code-only fix (worker stays v5.40.3); ai-meta pointer bumped β†’ v3.39.6; #123 closed + board 3 β†’ Done. PROD stays healthy on v3.39.5 + the live off-server cutover (NOT redeployed β€” ships to prod on a future server rollout). #127 stays OPEN.
  • 2026-06-22 β€” βœ… #121 SECOND-HALF SHIPPED + CLOSED β€” off-server system/* WAL-chain wedge fully fixed (server v3.39.5). server#257 (squash 54ac277 β†’ v3.39.5 c421273) gates BOTH off-server-drive sites in trigger_orchestrator_inner on should_publish(catalog_id) so system/* execs drive server-built run_state; regular execs keep the off-server path. #256 was only the first half (#121 reopened after the v3.39.4 prod re-cutover still wedged system/scheduled_cleanup on the system-pool worker). Pure is_system_path helper + unit test; 612 tests + clippy; kind full-gate BEFORE 9Γ— WAL chain incomplete β†’ AFTER COMPLETED 0 loops + regular loop still off-server. Server-only (worker stays v5.40.3); ai-meta pointer bumped; #121 closed + board 3 β†’ Done. PROD re-cutover = Phase-B follow-up this session.*
  • 2026-06-21 β€” βœ… #121 first-half SHIPPED (partial) β€” off-server WAL-chain-incomplete loop on system/ execs (link gate-off command.claimed through ChainHeads + don't route system/ execs to the off-server WAL drive); server v3.39.4; server-only; live prod off-server cutover wedge is the real-world repro.
  • 2026-06-21 β€” βœ… #125 + #126 SHIPPED + CLOSED β€” task_sequence do: jump/break/retry control flow + http tool body data-shape fixed (noetl-tools v3.14.0 tools#73 + v3.13.1 tools#72); worker v5.40.3 adopts the fixes (worker#124). Full 10Γ—1000 batch pft_flow_test now passes end-to-end on kind, correctness-clean (zero patient loss). Perf follow-up #127 filed. #121/#123 stay OPEN.
  • 2026-06-21 β€” βœ… #124 SHIPPED + CLOSED β€” distributed task_sequence forward set:/sibling bindings no longer render empty; defer sub-task templates with unresolved vars to the worker re-render (render_value_deferring_unresolved); server v3.39.3 (server#255). #121/#123/#125/#126 stay OPEN.
  • 2026-06-21 β€” βœ… #120 SHIPPED + CLOSED β€” reduce barrier no longer deadlocks (commands=0) on open/asymmetric loop joins (orchestrate-core); runtime liveness filter in the barrier (server v3.39.2 28e8950). server#254 (squash fbb855f); test test_open_loop_back_edge_does_not_block_dispatch, 133/133 + clippy, kind-validated 2Γ—2 matrix COMPLETE; PROD/defaults untouched. #121/#123 stay OPEN.
  • 2026-06-20 β€” βœ… #116 program-scale step 2 SHIPPED + multi-replica gate-ON validated β€” execution-affinity single-owner WRITE ORDERING. server#252 v3.39.0 + e2e#71. Step 1 (KV coherence) was necessary-not-sufficient β€” the command.issued prev-read + head CAS-advance are two non-atomic steps, so concurrent cross-replica emits forked the chain. Affinity routes every trigger (POST /api/events, which fires the drive) to the single replica that ShardConfig::owns(execution_id) owns; a non-owner forwards a reverse-proxy POST (one-hop loop guard, degrade-to-local). On the owner the single-process drive lock + in-memory ChainHeads make the readβ†’advance atomic, no distributed lock; KV is the genesis/handoff vehicle (owner resolves LOCAL β†’ kv_remote_hitβ†’0). server#252 (5e00d0a, v3.39.0): src/affinity.rs, flags NOETL_EXECUTION_AFFINITY/NOETL_PEER_URL_TEMPLATE/NOETL_SHARD_INDEX_FROM_HOSTNAME (all default off), metric noetl_execution_affinity_total{outcome}. e2e#71 (66b6e1b): 2-replica StatefulSet topology + rig HARD gate. Multi-replica gate-ON kind PASS: linear/loop/fanout COMPLETE, every chain roots=1/dangling=0/walk==total (NO fork), forwarded_ok +9, never-scan (scans+0) + sole-writer across replicas; single-replica unchanged; 595 server tests + clippy green; baseline restored. Follow-up #117: off-server from_events spine ordered by event_id wedges fan-in under a chain-orderβ‰ id-order inversion (affinity + high-concurrency fanout) β€” fix = order spine by prev_event_id walk; linear/loop already reliable. Prod multi-replica verdict: write-ordering COMPLETE (no fork) β€” prod can horizontally scale the off-server stack for linear/loop; high-concurrency fan-out needs #117 first. All affinity flags default off; PROD GKE untouched.
  • 2026-06-20 β€” βœ… #115 program-scale step 1 SHIPPED β€” multi-replica coherence DATA LAYER (NATS-KV-backed ChainHeads + ExecDescriptor); execution-affinity STAGED. server#251 v3.38.0 + e2e#70. NOETL_REPLICA_COHERENCE=nats_kv (default local, prod unchanged) backs the off-server drive's watermark + descriptor with JetStream KV buckets so 2+ replicas resolve the same value β€” head advance = CAS (one chain), descriptor = CAS merge; in-process maps = write-through cache / degraded fallback (local β†’ bit-identical). server#251 (8f39a79): src/coherence.rs (CoherenceKv + lazy buckets), ChainHeads/ExecDescriptors async, ExecDescriptor serde, metric noetl_replica_coherence_total{structure,op,outcome} (proof kv_remote_hit). e2e#70 (e222877): kind_validate_replica_coherence.sh + un-staled the #113/#114 offload asserts behind NOETL_RIG_EXPECT_OFFLOAD (default false; under refs_in_state=true the offload paths legitimately stay flat). Kind-validated: single-replica nats_kv is bit-for-bit parity with local (linear/loop/fan-out Γ—2 all COMPLETE, roots=1/dangling=0/walk==total, state_build_event_scans +0, hotpath scan +0, sole-writer intact); 2-replica proved cross-replica resolves work (kv_remote_hit advanced for head + descriptor, no kv_unavailable). Necessary but NOT sufficient: on 2+ replicas concurrent cross-replica emits still fork the chain (the issuing_event head-read vs head-advance is non-atomic across replicas β†’ observed forked chains + a cross-execution prev), so executions don't reliably COMPLETE on 2+ replicas yet β€” the remaining piece is execution-affinity (one replica owns an execution's drive + chain write; substrate present in src/sharding.rs shard_for/owns), STAGED as program-scale step 2. 588 server tests + clippy green; baseline restored. ai-meta pointers β†’ server 8f39a79 (v3.38.0) + e2e e222877. PROD GKE untouched; default local; no gate/mode/builder default changed. Off-server architecture is multi-replica-COHERENT (data) but not yet multi-replica-COMPLETE (write-ordering) β€” prod cutover stays single-replica until affinity lands.
  • 2026-06-20 β€” βœ… #115 Phase 5 SHIPPED + gate-ON validated β€” atomic-working-item context (tenet 6): the drive hands a worker only its minimal declared slice; NOETL_ATOMIC_ITEM_CONTEXT (default off). #77 (Explicit Input Binding) resolved. server#250 v3.37.0 + worker#121 v5.40.0 + e2e#69.
  • 2026-06-20 β€” βœ… #115 Phase 6 SHIPPED + gate-ON literal-zero validated β€” hot-path noetl.event read class RETIRED; the table is AUDIT-ONLY (server v3.36.0). NOETL_EVENT_READ_PATH=event_scan|audit_only (default event_scan, prod unchanged) retires the remaining lifecycle readers of noetl.event (the WHERE execution_id replay class outside the drive). server#249 (b71ca1d): under audit_only get_catalog_id (per-ingest) + inherit_parent_trace + subscription dedup-audit + container-callback catalog/existence serve from the in-memory execute-time ExecDescriptor; a cold descriptor (post-terminal straggler after eviction / restart) resolves catalog_id from noetl.command (synchronous queue) β€” never a noetl.event scan. Proof metric noetl_event_hotpath_reads_total{site,outcome}. ops#199 (e5b0737) pins event_scan on the prod server manifest (operator-gated flip). e2e#67+#68 (0ab3c0a) kind_validate_event_read_path_phase6.sh. Gate-ON kind-validated (PUBLISH_ONLY + offserver + materializer + audit_only): hot-path scan Ξ”0 (served_descriptor +96 + served_command +3), drive state_build_total Ξ”0 + event_scans Ξ”0 β‡’ ZERO noetl.event scans anywhere on the hot path, end-to-end; linear/loop/fan-out/output_select COMPLETE; sole-writer + lag-0; audit still works (direct SELECT + status COMPLETED + replay event_count=25); committed gate rig PASS with audit_only on (no regression); 585 server tests + clippy green; baseline restored. Completes the RFC never-scan end state (tenet 3) under the flag. ai-meta pointers β†’ server b71ca1d (v3.36.0) + ops e5b0737 + e2e 0ab3c0a. PROD GKE untouched; default event_scan; no gate/mode/builder default changed. Remainder = Phase 5 (atomic-item, needs #77) + program-scale (per-shard WAL, multi-replica descriptor coherence).
  • 2026-06-19 β€” βœ… #115 Phase 4 KERNEL + FLAG SHIPPED + shadow kind-validated β€” off-server state builder (worker v5.37.0 + server v3.33.0); drive cutover staged. The pool-side state_builder (worker#118, fef961c) reconstructs WorkflowState from the noetl_events WAL β€” a per-execution chain index walks prev_event_id headβ†’root, caches the spine keyed by the immutable chain head, advances only the new tail. A live WAL shadow loop (NOETL_STATE_BUILDER_SHADOW, default off) + the server NOETL_STATE_BUILDER=offserver|server flag (server#246, 3e6006d, default server). Gate-ON kind-validated on live cluster: shadow replayed the WAL β†’ chain-walked spines whose indexed==spine sizes match Phase-3 topologies (linear 13, loop 62, fan-out 25, output_select 31, storage_tiers 55) = parity by construction; WAL-read wal_events_total=993 with event_scans_total=0; cache cold_rebuild=28 (replay/restart) + incremental=21 (live tail-advance β€” fresh fan-out indexed Incremental(5), indexed==spine==25==DB event_rows 25); fresh fan-out COMPLETED gate-ON, event_rows==distinct, 0 __orchestrate__ event rows, materializer pending=0/project_errors=0; 8 worker unit tests + 2 server config tests + clippy green; baseline restored. ai-meta pointers β†’ worker fef961c + server 3e6006d. PROD untouched; no default changed. The offserver drive cutover (drive consumes the builder's state) + Phase 5 (atomic-item context, #77) / Phase 6 (retire event read path) remain.
  • 2026-06-19 β€” βœ… #115 Phase 3 MERGED β€” chain-walk state builder (server v3.32.0); Phase 4 (off-server state builder + WAL cache) started. server#245 self-merged (no classifier block) β†’ server v3.32.0 (8338417); ai-meta repos/server pointer bumped. Behind NOETL_STATE_BUILD_MODE=chain_walk (default event_scan, prod unchanged) the drive reconstructs WorkflowState by walking prev_event_id headβ†’root (in-memory ChainHeads head + (execution_id,event_id) PK lookups β€” never a WHERE execution_id scan) β†’ same from_events (orchestrate-core unchanged; parity by construction); falls back to event-scan on cold-head / lag / non-genesis. Gate-ON kind-validated (prior session): parity 41/41 MATCH, event_scans_total=0 / 1064 PK hops / 0 fallbacks, all topologies COMPLETE, sole-writer + lag-0 + gate rig PASS, 577 tests + clippy green. Phase 4 now in progress β€” move the chain-walk state construction OFF the server onto the system worker pool reading the WAL/NATS stream, pool-side cache keyed by the immutable chain head + incremental tail-advance (server chain-walk + event-scan remain fallbacks). PROD GKE untouched; no gate default changed.
  • 2026-06-19 β€” βœ… #115 Phase 2 implemented + kind-validated β€” one-level prev_event_id event chain (server#244 + noetl#667 merged). Each noetl.event carries prev_event_id (the immediately-previous event in causal order) + each noetl.command the issuing-event link, so per-execution events form a walkable singly-linked list followable pointer-by-pointer without scanning noetl.event (additive; no reader yet β€” Phase 3). The emit chokepoint emit_events stamps the event link from a per-execution chain-head watermark (ChainHeads) β€” one path covers drive events + command.issued + worker-lifecycle on both the gate-off INSERT and the gate-on publish, the materializer persisting it; the command link = the real step.enter/unblocking completion so cursor-fan-out bodies share their branch origin (Β§4.4). Server-only (no orchestrate-core change). Chain-correctness proven gate-ON across 6 executions (linear 13/13, loop 62/62, fan-out 25/25 with a real shared branch origin, sub-playbook 46/46, + Phase-1 output_select 31/31 & storage_tiers 55/55 bounded): each has 1 root, 0 dangling / 0 duplicate event prev, 1 head, pointer-walk == full sequence (no gaps), real-step command dangling=0; kind_validate_orchestrate_gate.sh PASS (sole-writer 25==25, 0 dup cycles, catalog0=0, lag 0); 573 lib tests + clippy green. PROD untouched; no gate default changed. Awaiting merge β†’ ai-meta pointer bump; Phase 3 (chain-walk state builder) next.
  • 2026-06-19 β€” 🐞 #114 oversized-command.issued offload shipped (server v3.29.5); refs_in_state consume side (#101) is the remaining off-server-drive cutover blocker. With #113's decode fix in place, 4 large-context fixtures wedged at a distinct second stall: under refs_in_state=false the off-server drive embeds the full resolved upstream context into the next command, so its command.issued event (~1.32MB) exceeded NATS max_payload β†’ publish ack-timeout β†’ wedge. Fix: a command context over NOETL_COMMAND_CONTEXT_MAX_BYTES (512KB) is offloaded to noetl.result_store with a {__context_ref__} marker; get_command/claim_command resolve it before the worker sees it (metrics context_offloaded/context_ref_resolved). Server #242 β†’ v3.29.5 + rig phase 8 e2e#64. Kind gate-ON: off-server rig PASS (new test_oversize_command_context COMPLETED, max command.issued ctx 585B, offload+resolve fired, 0 __orchestrate__ event rows, materializer lag 0); every command.issued event <1MB across all fixtures; 6 of #113's 9 now COMPLETE. Chose ref-on-oversize over refs_in_state=true (candidate #1): a kind experiment proved refs_in_state=true fixes the state-bloat (kind_playbook_lease_expiry completes β€” drive-state 29KB vs ~1MB loop) but breaks the bulk-consuming fixtures (test_storage_tiers/test_output_select fail at the bulk step) because the worker render-time ref-resolution isn't implemented β€” so the default stays false. The remaining 3 fixtures + the cutover (#107/#111) now hinge on the refs_in_state consume side (#101) β€” __orchestrate__ drive-state bloat (17.4MB for storage_tiers) + the _ref/bulk-resolve gap. ai-meta β†’ server 385d21f (v3.29.5) + e2e 9919392. No prod default flipped; prod is pre-#108 in-server drive (unaffected).
  • 2026-06-19 β€” 🐞 #113 off-server drive β€” recover offloaded drive result + stop drive on cancel (server v3.29.4); #114 opened. Fixed the worker-driven drive stall when an __orchestrate__ result exceeds the 100KB inline budget (worker offloads it with only a reference.ref β†’ server now resolves+decodes it via result_store.resolve, metric ref_resolved, instead of dropping β†’ non-convergent re-loop) + the cancel non-stop facet (match underscore playbook_cancelled + ExecutionState::is_terminal terminal guard, no restart). Server #241 β†’ v3.29.4 + rig e2e#63. Kind gate-ON proven (785KB result β†’ ref_resolvedβ†’COMPLETED, 0 decode WARNs; cancel froze a drive-loop instantly; sole-writer lag 0). 5/9 #113 large-context fixtures COMPLETE; the other 4 hit a distinct oversized-command.issued (full upstream context embedded β†’ >1MB NATS payload) stall β†’ #114 (#113 stays open until all 9 close). ai-meta β†’ server 1e844c1 + e2e 12b27e9. No prod default flipped.
  • 2026-06-19 β€” πŸš€ #103 GKE pre-flip PREP β€” prod images pushed, GMP monitoring live, manifests staged; NO traffic flip / NO PUBLISH_ONLY. Verified prod already-Rust (the #49 cutover is done; pre-#103 live images), both flip secrets present, monitoring = Google Managed Prometheus (not VM). Pushed server v3.29.3 + worker v5.35.0 to the prod AR (amd64); applied + verified GMP PodMonitoring (worker+server /metrics β€” the noetl ns had none) + materializer-lag Rules (up{namespace="noetl"}=4 live); staged the roll-forward manifests (not applied β€” they roll live workloads); runbook gained a "Production (GKE)" section + GMP managedAlertmanager pager stub. Operator-gated: roll images β†’ materializer shadow β†’ pager β†’ flip. ai-meta e5b6d6c β†’ ops 9edd9c4 (ops PR #197). No prod default changed.
  • 2026-06-19 β€” πŸ›‘οΈ #103 materializer-lag GUARDRAIL shipped β€” the pre-flip observability gate. The server was FLIP-READY; the remaining gate was a materializer-lag metric + alert. Worker #116 β†’ v5.35.0 extends the JetStream lag poller to track the noetl_events/noetl_materializer consumer on an independent task β†’ noetl_worker_nats_consumer_pending{consumer="noetl_materializer"} climbs even when the materializer loop is dead. Ops #195+#196: VMRule (backlog warning>200/critical>2000/growing + stall-under-gate + project-errors + absent-under-gate, stall guarded on backlog>0), worker /metrics VMServiceScrape (was unscraped), VMAlert enabled, Grafana dashboard, flip runbook noetl-cqrs-publish-only-flip.md. Kind-proven full cycle on the VM stack: green baseline (backlog 0) β†’ induced lag (materializer fault-injected, events publishing under the gate) β†’ gauge 0β†’684, alerts fire (backlog warning+critical + stall) β†’ recover β†’ drainsβ†’0 idempotently (0 dup/loss), alerts clear. ai-meta β†’ worker b910341 (v5.35.0) + ops 2fcfa59 + worker-wiki 0030f30. PUBLISH_ONLY stays default-off.
  • 2026-06-19 β€” 🎯 #103 server cutover COMPLETE β€” FLIP-READY. The 2 ExecutionService cancel/finalize sites now route through the emit_event chokepoint (server #240 β†’ v3.29.3 + e2e rig #62); kind-proven both modes; no remaining synchronous server event writers under the gate. Flipping PUBLISH_ONLY on is now a staged operator decision.
  • 2026-06-19 β€” 🎯 #104 off-server-drive Γ— gate reconciliation PROVEN β€” the last real blocker before the PUBLISH_ONLY flip is operator-safe. The combination #103 left unproven (gate-on was only ever validated with the in-process drive) is green on kind: gate-ON (PUBLISH_ONLY=true) with the off-server drive (PLUGIN_DRIVE=true) + materializer sole writer β†’ fresh exec + cursor fan-out β†’ COMPLETED; server wrote 0 noetl.event rows (all 25 PUBLISHED β€” event_ingest_published_total=25), materializer materialized all 25 exactly once (25 rows == 25 distinct ids, 0 catalog_id=0, 0 dup cycles), drive dispatched=applied, read-your-writes held (the relocated trigger fires post-materialize β†’ server rebuilds state from committed log before bounding the off-server drive input). Server #238 β†’ v3.29.2 (76d29bb): cold-cache apply now rebuilds WorkflowState from the durable log (the #104 WAL-rebuild principle) instead of dropping the in-flight result on a server restart mid-drive β€” kind crash-recovery proof: hard-kill mid-drive β†’ cold_rebuild metric+log fires β†’ that exec COMPLETES with full event integrity. Committed e2e rig kind_validate_orchestrate_gate.sh (e2e#61). Regression green: gate-off + in-process (prod default) and gate-off + off-server. ai-meta β†’ server 76d29bb + e2e 61f7a5c. Remaining before a safe flip: only the 2 ExecutionService cancel/finalize sites. No prod default changed.
  • 2026-06-18 β€” 🎯 #108 (c) β€” the worker-driven orchestrator drive is now the DEFAULT; #108 CLOSED. Flipped NOETL_ORCHESTRATE_PLUGIN_DRIVE to default true (server#233, v3.28.0 β†’ server@80cc0e6). Gated on a scale soak on kind (images built from the released tips, server v3.27.0 / worker v5.33.0): a single 694-drive cursor+fan-out run (test_pft_flow_v2 3Γ—40) COMPLETED with __orchestrate__ rows in noetl.event = 0 (event_suppressed +2082) and all 694 drives claimed on the system pool (shared pool got only the 671 real-step commands), 0 errors; 5Γ— concurrent self-contained cursor = 5/5 COMPLETED. Then deployed the flipped image with no env var β€” the default-on path reproduced the identical shape (361 drives, system-isolated, 0 burst); 15/15 regression fixtures green; the revert (=false) verified to fall back to the in-process drive (system delta 0). In-process trigger_orchestrator_inner kept as the fallback. ai-meta β†’ server 80cc0e6 + worker 437b0be + server-wiki 0210012.
  • 2026-06-18 β€” orchestrate drive isolated on the SYSTEM pool via pool affinity (#108 follow-up b, kind-validated). Server stamps execution_pool on the command notification (server#232 β†’ server@846166b); worker declines (ACK+skip) notifications not for its pool segment (worker#114 β†’ worker@e2162b7), so the drive runs on the dedicated system pool even under JetStream consumer-filter drift. No worker HTTP pending-poll β€” the NATS consumer is the only claim vector. Validated: __orchestrate__ claimed+executed on the system pool (3), zero on the default pool, simple_python COMPLETED; 553+196 tests green. Only (c) the deliberate default-flip remains.
  • 2026-06-18 β€” the orchestrate meta-command touches noetl.event ZERO times (#108 follow-up a, kind-validated). dispatch_orchestrate_command stops writing command.issued to noetl.event; the command lives only in noetl.command, and claim_command/get_command fall back to it on a miss (noetl.event stays authoritative for normal commands) (server#231 β†’ server@9438f3b). So __orchestrate__ writes 0 of its former 5 rows per drive β€” the directive that system playbooks keep only their own state is met. Validated: cursor+fan-out COMPLETED via the noetl.command fallback, 0 event rows, 20 real steps normal, 0 errors. Remaining: NATS affinity (ops) + default-flip.
  • 2026-06-17 β€” system playbook events no longer burst Postgres + system-pool routing (#108 slice 4b, kind-validated). The __orchestrate__ meta-command is infrastructure, not a workflow step, so the server now skips persisting its lifecycle events to noetl.event (handle_event_inner + claim_command) (server#230 β†’ server@6aef3a6). At scale they'd burst noetl.event/Postgres for no benefit. Validated: __orchestrate__ now writes only the lone command.issued (1 of 5 β€” 80% fewer rows); cursor+fan-out flow still COMPLETED. Drive routes to the system segment (true isolation pending a NATS-affinity ops fix; resilient via the pending-poll meanwhile). Follow-ups: eliminate the last command.issued (claim from noetl.command) + NATS affinity.
  • 2026-06-17 β€” 🎯 the orchestrator drive runs OFF-SERVER on the worker pool (#108 slice 3, kind-validated). With NOETL_ORCHESTRATE_PLUGIN_DRIVE=on the server issues system/orchestrate (entry: run_state, args = the bounded WorkflowState) to the worker pool instead of evaluating in-process; the worker runs the drive, the server applies the result on the command's call.done (server#229 β†’ server@465cdbb v3.23.0). Kind: test/simple_python drove startβ†’endβ†’COMPLETED through the round-trip (dispatched=2, applied=2, 0 decode_error, __orchestrate__ didn't leak as a step, playbook.completed). Default off, in-process fallback. Bug caught+fixed: output_b64 rides call.done not command.completed. Next: shadowβ†’flip at scale, make drive the default, route to the system pool.
  • 2026-06-17 β€” worker-driven cutover slice 2: apply_orchestration_result extracted + slice 3 designed (#108). The post-evaluate emission (events β†’ commands β†’ terminal) is extracted verbatim from trigger_orchestrator_inner into a reusable fn (server#228 β†’ server@586aeae) so the worker-driven drive applies a worker-computed result identically. Behavior-preserving (553 tests green, clippy clean). Slice 3 (dispatch) designed + grounded: apply_event would phantom-step a meta-command, so the design uses a reserved __orchestrate__ step ignored in state, a flag-gated scheduler, apply-on-callback, and loop-prevention. Lands behind NOETL_ORCHESTRATE_PLUGIN_DRIVE (default off), kind-validated before the flip.
  • 2026-06-17 β€” worker-driven cutover slice 1: configurable wasm guest entry (#108). The worker can now dispatch a named plug-in export (worker#113 β†’ worker@04420d0): tool: {kind: wasm, plugin: {path, version, entry}} names the export (default run); the worker-driven orchestrator will use entry: run_state. invoke_bytes_with_entry + run_by_ref_entry/run_and_apply_by_ref_entry (originals delegate with run); test proves runβ†’0xAA vs run_stateβ†’0xBB + missing-export error. Purely additive, no live change. Server scheduler+apply (the hot-path round-trip, default-off flag) next.
  • 2026-06-17 β€” orchestrate plug-in drives the real workload identically, live (#108 slice 4). The orchestrator runs the plug-in alongside the in-process drive on every evaluation + diffs commands (server#227 β†’ server@bd652ab) β€” a process-global wasmtime host (feature orchestrate-shadow) loaded from noetl.plugin_module at boot, gated NOETL_ORCHESTRATE_PLUGIN_SHADOW; in-process result authoritative. Kind-validated over the live 10Γ—1000 PFT: noetl_orchestrate_shadow_total{result="match"} 529, ZERO mismatch/error, workers stable. Plug-in gains a state-input path (run_state); both build configs green (default no wasmtime). Slices 1-4 prove orchestrator-as-plug-in end to end; next is the worker-driven cutover.
  • 2026-06-17 β€” system/orchestrate@1 registered + servable in a deployed server (#108 slice 3). The server bakes the orchestrate wasm into its image and seeds built-in system plug-ins into noetl.plugin_module on boot (server#226 β†’ server@b21b589, kind-validated). New src/system_plugins.rs (pure dir-scan + sha256, unit-tested) + a wasmbuilder Docker stage + NOETL_SYSTEM_PLUGIN_DIR; in-process upsert (not the token-gated HTTP surface); digest-keyed hot-reload. Validated: GET /api/internal/plugins/system/orchestrate?version=1 β†’ 200 application/wasm 1559093 bytes, ETag=digest, staleβ†’409, baked sha256 == served digest. Next: kernel scheduler dispatches the now-registered plug-in.
  • 2026-06-17 β€” system/orchestrate plug-in runs identically to native in wasmtime (#108 slice 2). A wasmtime shadow-diff (server#225 β†’ server@ccec104) loads the built .wasm through a harness mirroring the worker host's invoke_bytes ABI byte-for-byte and asserts the wasm output equals the native drive over auth0 multi-arc when: routing (minijinja in wasm) + cold-start. Finding: command-set identity (parsed Value eq), not raw bytes β€” the context map serializes in insertion order (serde_json preserve_order ← upstream HashMap iteration, differs wasm32 vs host arch); the scheduler deserializes to Vec<Command>, so the command set is the bar. 2 unit + shadow-diff green; plug-in excluded; test-only. Next: catalog register/serve β†’ kernel scheduler.
  • 2026-06-17 β€” system/orchestrate WASM plug-in exists β€” drive core runs as a 0-import module (#108 slice 1). New standalone plugins/orchestrate/ crate (server#224 β†’ server@10a629b) wraps the drive behind the worker plug-in ABI (input = JSON event-slice + playbook; output = JSON OrchestrationResult; data-plane = memory/alloc/run) and compiles to wasm32-unknown-unknown β€” the first non-trivial compiled system playbook. Feasibility risk retired: the .wasm has 0 imports (no WASI, no host render) β€” the whole drive incl. minijinja runs in-guest. Native parity test reproduces native evaluate byte-for-byte; 551 server tests green, crate excluded from the workspace. Next: worker-host shadow-diff β†’ catalog register/serve β†’ kernel scheduler (NOETL_ORCHESTRATE_PLUGIN, default off).
  • 2026-06-17 β€” Orchestrator drive core fully wasm-resident β€” Event-ABI round #109 CLOSED. Slice 3 (server#223) moved orchestrator/evaluate from src/engine/ into noetl-orchestrate-core. All 6 drive modules (renderer, playbook model, commands, evaluator, state, orchestrator switch) now compile native + wasm32-unknown-unknown β€” the system/orchestrate plug-in seed (#108). evaluate reads the pure core::event::Event; server converts db::Event at the trigger_orchestrator boundary (slice-1 From). 122 core + 565 server tests green, 0 WASI imports on wasm32, clippy clean; cargo-chef image (v3.20.0) kind-deployed, PFT 10Γ—1000 β€” full command lifecycle, 0 errors, 0 restarts. ai-meta β†’ server bfd3f77 (internal refactor, stays v3.20.0).
  • 2026-06-14 β€” Transfer tool: Snowflake↔Postgres both directions β€” #99 CLOSED. Both transfer arms implemented with full credential-alias resolution. tools v3.10.0 (tools#65) + worker v5.22.0 (worker#87) + e2e#58. SFβ†’PG: $n::text::<udt> coercion + RFC3339 timestamp reformat; PGβ†’SF: generated INSERTs. Full bidirectional data_transfer/snowflake_postgres fixture COMPLETED on kind against live sf_test account. tools β†’ 4127b4b Β· worker β†’ 6d97e7c Β· e2e β†’ 94aa7f1.
  • 2026-06-14 β€” Snowflake key-pair JWT validated end-to-end β€” #98 last external-tool gap closed; transfer step β†’ #99. noetl-tools v3.9.0 / v3.9.1 / v3.9.2 (tools#62/#63/#64) β€” key-pair JWT auth (bypasses MFA) + User-Agent fix (code 391903) + SQL-API context-in-body + multi-statement split (codes 391911 + 000008). Worker bumped to v3.9.2 (worker#83–#86) + e2e#57 fixture cleanup. create_sf_database (CREATE DATABASE) + setup_sf_table (CREATE TABLE + INSERT) both COMPLETED via JWT on kind against the live sf_test account (NDCFGPC-MI21697). Transfer step fails (inline creds, no key-pair fields) β€” filed #99. ai-meta pointers: tools a216ab2 Β· worker 9d6b127 Β· e2e e191231.
  • 2026-06-12 β€” #90 Phase 7 shipped β€” scale hardening; #90 CLOSED (all 7 phases complete), live proof green. Final phase: server v3.5.0 (server#189) POST /api/execute/batch (Nβ†’N, partial-failure contained) + opt-in exactly-once dedup window (noetl.subscription_dedup, bounded-by-age, race-safe, default off); worker v5.19.0 (worker#79) batch dispatch + dedup opt-in + per-subscription rate limits (deterministic token-bucket RateGovernor, fetch-side backpressure β†’ source keeps backlog, no loss, subscription.rate_limited event); ops (ops#176) + e2e (e2e#48); no tools change β†’ no crate cascade. Live on kind: batch 12β†’12 COMPLETED on the subscription pool + per-message traceparent; dedup duplicateβ†’1 execution + subscription.message.deduplicated; rate-limit engaged + 10/10 β†’ executions (no loss); direct-curl within/outside-window + dedup-off + batch partial-failure all green. ai-meta β†’ server 7b217d8 + worker 7531f4a + ops 6db69b9 + e2e 203593b. #90 closed; follow-ups tracked: #91–#94 + tools#57.
  • 2026-06-12 β€” #90 Phase 6 shipped β€” CLI local noetl subscribe + FileEventSink + local_disk spool (live local proof green). Added noetl subscribe <spec.yaml> (cli v4.11.0, cli#60, closes cli#59): a kind: Subscription listener run standalone in local mode β€” no k8s, no NATS-dispatch server for the listening itself β€” reusing the same noetl_tools source clients + directive engine + spool engine the in-cluster worker uses, emitting the same ExecutorEvent envelope to a local FileEventSink (one event/line JSONL β†’ replayable trail). Local dispatch (RFC Β§5.3): in-process via PlaybookRunner (pure-local default) or POST /api/execute. local_disk spool (Β§8.6): circuit-breaker + buffer + ordered replay + idempotency + dead-letter against a local dir, circuit state in a local file. New src/subscribe/{mod,spec,sink,dispatch,runtime,spool}.rs + examples/subscribe/. cli-only β€” no tools change / crate cascade (the source+spool surface ships in noetl-tools v3.5.0; bumps the lock 3.0.0 β†’ 3.5.0 via the executor's "3"). Tests: 12 subscribe + full bin suite (53) green, incl. a deterministic outageβ†’spoolβ†’ordered-replayβ†’idempotency proof on the real engine. Live (in-cluster NATS on kind): 5 msgs β†’ received=5 dispatched=5 failed=0 (19-event JSONL trail); local_disk spool outage β†’ 6 message.spooled (0 dispatched, no loss) β†’ recovery β†’ 6 message.replayed in order β†’ drained to 0. Finding: the NATS source ignores URL-embedded user:pass (async-nats ConnectOptions) β€” specs use explicit user/password. ai-meta β†’ cli 2fb3fb0 (v4.11.0); wiki cli subscribe. #90 stays open for Phase 7 (scale hardening, volume-gated).
  • 2026-06-11 β€” #90 Pub/Sub + Kafka brought to live-E2E parity with NATS (validation gap closed). Stood up the two remaining subscription brokers in kind β€” Pub/Sub emulator (gcloud SDK image) + single-broker KRaft apache/kafka:3.9.1 β€” under noetl/ops (ops#170), and added bounded-drain fixtures + kind-validate runners under noetl/e2e (e2e#41). Both backends passed the same live bar as NATS: publish/produce 5 β†’ bounded drain count=5 acked=true β†’ execution COMPLETED β†’ call.done/command.completed/playbook.completed event trail. No adapter code change needed β€” the pure-Rust kafka crate talks to Kafka 3.9 KRaft and the Pub/Sub REST backend works against the emulator as-is. The one fix: the <step>.output.<field> accessor never resolved (both when: arcs skipped β†’ drain stalled); corrected to <step>.<field> in the fixtures + the latent ops subscription_drain.yaml example. Validated on server v3.1.0 + worker v5.15.2 + tools v3.2.0; cluster left on that clean released stack. ai-meta β†’ ops 568a4ac + e2e 8d21e7a. #90 stays open (Phases 2–7 design-only).
  • 2026-06-11 β€” #89 shipped β€” JSON null round-trips through {{ step }} (server fix, v3.0.6). #89 β€” nullβ†’undefined serialization β€” CLOSED. The #88 cursor fixture walked all 4 pages but its 4th check_pagination crashed: the terminal page's next_cursor: null, re-injected via the whole {{ fetch_page }} envelope, rendered as the JS token undefined (invalid JSON), so the consuming Python step received response as a str. Traced the corrupt command.issued args.response to the renderer that builds next-step inputs β€” the server orchestrator (src/template/jinja.rs::render_to_value), not the worker the issue blamed. json_value_to_minijinja maps JSON nullβ†’Value::UNDEFINED; minijinja's map repr emits undefined; render_to_value failed from_str and fell through to a raw string. The noetl-tools engine already had a | tojson retry for exactly this; the server's copy had diverged without it. Fix (server#177, v3.0.6) ports the retry. 5 new tests; 619 lib + 8 parity green; clippy clean. Kind-validated end to end on the live test-server (baseline 4th check_pagination error β†’ fixed success; cursor collects 35, matching offset). ai-meta β†’ server 8e17fbe. Standing direction honored β€” Claude wrote the Rust directly, no Codex.
  • 2026-06-10 β€” #88 shipped β€” pagination fixtures read response.body.*; #89 filed. #88 β€” offset/cursor pagination fixture path β€” CLOSED. The Rust http tool nests the parsed JSON payload under body ({{ fetch_page }} β†’ {body, headers, status_code}); the fixtures read response.get('data', {}), which resolved to {}, so has_more/next_cursor defaulted falsy and the loop exited after page 1 despite the correct post-#85 machinery. Confirmed the shape against a live http-tool result, then switched both check_pagination steps to response.get('body', {}) (e2e#40). Kind-validated: offset walks 0β†’10β†’20β†’30, users 10/10/10/5, validate_results success 35, playbook.completed COMPLETED; cursor path-fixed + walks all 4 pages (Mg==β†’Mw==β†’NA==β†’null, 35 events fetched) but the terminal page surfaced a distinct worker bug β†’ #89 (worker serializes next_cursor: null as JS undefined when re-injecting {{ fetch_page }}, so the consuming Python step gets an unparseable str). Other pagination fixtures (retry/max_iterations/pipeline*/loop_with_pagination) share the same envelope-key assumption over /api/v1/assessments|flaky ({data, paging}) β€” flagged, left for follow-up. ai-meta β†’ e2e 72a7525.
  • 2026-06-10 β€” #87 shipped, #85 deferred (e2e sweep follow-ups #85/#87). #87 β€” multi-tool sibling references β€” CLOSED. task_sequence (the tool: [list] pipeline runtime) stored each sub-tool's result for the aggregated output but never injected it into the running context, so a later sub-tool's {{ <label>.<field> }} rendered empty β€” masked in quoted positions, a syntax error at or near "," in unquoted numeric SQL (save_edge_cases test_large_payload). Fix (tools#48, v3.1.1) injects each sub-tool's result under its label (synthetic .data self-ref); worker adopts via worker#69. Kind-validated on a worker built from the fix: save_edge_cases test_large_payload β†’ record_count = 100 (no syntax error), save_delegation_test clean. ai-meta β†’ tools 76f942a + tools-wiki 4962f8b + worker b97f642. #85 β€” workflow-arc loop re-entry β€” DEFERRED (kept open). Implemented the dispatch-guard layer (draft server#176): a back-edge detector (cycle + recency) re-enters a completed loop head, so the loop no longer hangs (608 lib tests + 5 new pass). But kind validation surfaced a second blocker β€” set: ctx.X loop variables are recomputed per orchestrator pass and revert to the workload default when the producing step is re-dispatched (a minimal counter-loop thrashes 0,0,1,0,1,2,…). Full multi-page pagination needs durable event-sourced ctx propagation across iterations β€” larger than is safe to land well-tested in one session; held as a draft, not merged. Standing direction honored: Claude wrote all Rust directly (no Codex).
  • 2026-06-10 β€” #80 closed β€” container_callback chain green end to end. Fixing the watcher's missing curl (the literal #80 goal) surfaced two more layered bugs beneath it. Watcher image (ops#168): the manifest used the retired bitnami/kubectl:1.30.3 (removed from Docker Hub; the live cluster was patched to the bitnamilegacy archive) with a runtime apt/apk install step that never put curl on PATH β†’ callback POST returned HTTP 000. Switched to alpine/k8s:1.30.3 (kubectl + jq + curl baked in), dropped the install hack. Server insert (server#173, v3.0.3): once curl worked the POST reached the server and 500'd β€” the container-callback handler inserted call.done via a stale query targeting an attempt column that doesn't exist on the deployed noetl.event; fixed to the working handlers::events column set. OOM path: the watcher only read Job-level conditions so failed_oom could never fire β€” added pod-level OOMKilledβ†’failed_oom classification (ops#168); the completed_at fallback for failed Jobs used bare jq now (numeric epoch β†’ HTTP 422), fixed to RFC3339 now | todate; and the e2e fixture's bytes(40MiB) was calloc-lazy (mapped to the zero page, never faulted in) so the container exited 0 β€” switched to a written-into bytearray that dirties pages and reliably OOM-kills (e2e#38). Verified the kind cluster actually enforces memory limits (120 MiB in a 32Mi pod β†’ OOMKilled exit 137). Rebuilt the server image + reloaded into kind; kind_validate_container_callback.sh both probes GREEN β€” happy_path β†’ succeeded (delta 1), oom β†’ failed_oom (delta 1). This is the last blocker on the #43 container-callback chain. ai-meta β†’ ops cacc513 + server 5d2cf58 (v3.0.3) + e2e 6aaf06e.
  • 2026-06-10 β€” #79 closed β€” e2e kind-val runners back on the current noetl CLI surface. Both scripts/kind_validate_*.sh runners aborted immediately on error: unrecognized subcommand 'playbook' β€” they targeted the retired noetl playbook register/execute + noetl execution status/events verbs. The validation logic and the event taxonomy (step.enter / command.completed / node_name / the fan-in barrier) were intact; only the invocation layer had drifted. Fix (e2e#37): noetl register playbook --file, noetl exec <catalog-path> --runtime distributed --json (exec by metadata.path, not the bare name), noetl status <id> --json, and the event log over noetl query (no events verb today β€” rows wrap under .result, order by event_id since noetl.event has no timestamp column). Added a fail-fast CLI-surface guard to each runner. Validated on kind (server-rust v3.0.1 + worker-rust, :8082): fanout_reduce PASS start-to-finish with no manual workaround; container_callback drives registerβ†’execβ†’COMPLETED cleanly and stops at the metric-delta assertion because the deployed noetl-k8s-watcher image lacks curl (watcher.sh: curl: not found β†’ HTTP 000) β€” a cluster-side watcher gap tracked on #80. Version-skew note: PATH binary is noetl 2.17.0, repos/cli submodule is v4.10.0; the targeted surface is identical across both, so the runners work on either (the binary lags the submodule by a major line β€” worth refreshing for parity, not required here). Pointer: e2e β†’ a3594b3; e2e wiki: new Kind-Val Runners page.
  • 2026-06-10 β€” #82 closed β€” GUI credential View/Edit recovered for pre-wallet records. The Secrets Wallet (#61) moved credential storage to forward-only envelope encryption; pre-wallet records now 500 on GET /api/credentials/{id}?include_data=true (Decryption failed: aead::Error), so the GUI View/Edit flow dead-ended on a generic toast (response shape unchanged). Fix (gui#36): View surfaces the real reason + points to Edit; Edit still opens with the list-row metadata (name/type/description/tags) + a warning banner and an empty-but-required data field, so re-entering the secret and saving re-seals the record under the current wallet β€” recovering it. Validated live against kind + the dev:kind UI on :3001. Also landed e2e#36 (duplicate workload probe-flag keys removed from tooling_non_blocking) and gui#35 (dev:kind convenience script). Pointers: gui β†’ 8cacc9e (v1.11.1), e2e β†’ 4a9ffbc.
  • 2026-06-10 β€” #81 closed β€” noetl-server v3.0.2 fixes the container-tool command type contradiction. ToolSpec.command was Option<String> (scalar) but the container tool kind writes a K8s-Job-style array β€” an array failed the server's ToolDefinition untagged-enum match (400), a scalar was rejected by the worker's ContainerConfig.command: Option<Vec<String>>. Typed command as Option<serde_json::Value> (same as args); ToolCall::from_spec forwards it verbatim. 2 regression tests; clippy clean (server#172, v3.0.2). Kind-val GREEN end-to-end: server accepts the array command, worker creates the K8s Job, Job reaches Complete 1/1. Server pointer bumped (ai-meta β†’ server bd36672). Chain counter-bump validation stays gated on #79 (runner CLI) / #43.
  • 2026-06-09 β€” E2E sweep cleanup β€” noetl-tools v3.1.0 + noetl-server v3.0.1. Stripped the diagnostic tracing::debug! scaffolding added during the e2e triage, kept the production fixes: YAML when: true boolean + |tojson object-template fallback (tools#47), 64 MB result-store body limit + pipeline command/spec stash (server#171). Pointers bumped (ai-meta@316048c tools, @6590bd6 server); tracks #49. All 7 sweep playbooks PASS on Rust-only kind. Worker crates.io dep-revert deferred β€” v3.1.0 not yet on crates.io ([skip ci] release commit).
  • 2026-06-08 β€” noetl-tools v2.24.2 clippy cleanup + noetl/server#22 closed. Cleared the clippy -D warnings CI gate on noetl-tools (15 warnings across 7 files; all mechanical lint fixes). Closed stale noetl/server#22 (Phase D orchestrator engine port β€” complete). noetl/server PR #167 (same clippy shape) opened, awaiting merge.
  • 2026-06-05 β€” Rust-only regression rig β€” canonical v10 SQL + http config shapes. Swept ~30 self-contained e2e fixtures against the Rust-only kind stack and fixed three config-shape classes in noetl-tools: postgres command: alias + multi-statement SQL (tools#24, v2.18.3), a task_sequenceβ†’duckdb regression test (tools#25), and the duckdb command: alias + http params/headers/form non-string coercion (tools#26, v2.18.4). Worker adopted both (worker#50, worker#51). Newly GREEN: duckdb_test, json_serialization_save, duckdb_retry_query, pagination/{offset,cursor,max_iterations,pipeline}, retry_simple_config. Recovered the cluster first (server had latched into NATS not configured after a podman restart). Server-side follow-up noted: loop_with_pagination renders {{ execution_id }} empty in a multi-statement postgres command.
  • 2026-06-05 β€” postgres-tool observability β€” real SQLSTATE errors. noetl-tools 2.18.2 (tools#21) + worker dep bump (worker#49): the postgres tool surfaces the real SQLSTATE + message instead of the opaque db error. Validated end-to-end β€” a bad query reports ERROR: relation "..." does not exist (SQLSTATE 42P01) in the call.error event. Closes the last follow-up from the credential/iterator saga.
  • 2026-06-05 β€” iterator_save_test GREEN β€” full v10 + credential + iterator-pipeline surface validated. server#73 (v2.19.7) defers task_sequence _prev/_results refs at command-build so nested-pipeline templates render at runtime. iterator_save_test reaches playbook.completed and writes 3 rows to the real demo_noetl DB β€” the deepest v10 path (iterator β†’ pipeline β†’ _prev chaining β†’ nested credential β†’ postgres write). Closes the credential + iterator + pipeline chain (server#71, worker#46, worker#48, server#73).
  • 2026-06-05 β€” Nested-pipeline credentials + template-timing finding. worker#48 (v5.11.3) β€” the worker now pre-resolves keychain aliases on task_sequence SUB-tasks; iterator_save_test's nested save_item postgres step connects to demo_noetl. Closes the credential-path chain (store β†’ alias-key β†’ nested resolution, all validated). Last iterator_save_test blocker found + filed: server#72 β€” the server pre-renders task_sequence {{ _prev.* }} refs (runtime-only) to empty β†’ malformed SQL (a symptom the v2.19.5 Chainable change surfaced).
  • 2026-06-05 β€” Keychain-credential path validated on Rust-only. Continuing R5 Tier 4, registered the pg_k8s postgres credential and probed the DB-backed fixtures. Surfaced + fixed a 3-bug chain in the keychain subsystem: credential store bound AES-GCM Vec<u8> to a TEXT column (server#71, v2.19.6); alias resolution read only the auth: key not v10's credential: (worker#46, v5.11.2). Proven: iterator_save_test's create_table connects + runs DDL against the real demo_noetl DB. Third bug β€” nested-pipeline credentials (task_sequence sub-tasks bypass worker resolution) β€” filed as worker#47 for a follow-up round. Session details
  • 2026-06-05 β€” v10 control-flow runs end-to-end on Rust-only. Phase F R5 Tier 4 re-probe found + fixed 7 more bugs across the Rust stack (server v2.19.5 server#69 6 commits, worker v5.11.1 worker#44, tools v2.18.1). Four v10 fixtures now reach playbook.completed β€” start_with_action, end_with_action, loop_test, control_flow_workbook; actions_test correct-fails on a missing TEST_SECRET env. Root-cause chain: catalog SQL type drift β†’ ToolSpec null-serialization β†’ worker array-config drop β†’ orchestrator end-step skip + task_sequence label-wrap β†’ minijinja Lenient-vs-Chainable undefined β†’ end-step trigger gate. Also: rust-analyzer workspace setup + rule (ai-meta@38287b7). Session details
  • 2026-06-04 (late evening) β€” Rust-only e2e complete + legacy cleanup. Six interlocking server gaps closed in one iteration (#55–#60)
    • two noetl-tools fixes (#15, #16); worker dep bump; kind cluster legacy Python deployments retired. control_flow_workbook runs fully end-to-end on the Rust-only stack. Standing direction pinned: Rust-only focus, ignore Python tasks. Session details
  • 2026-06-04 (afternoon) β€” Pipeline + failure termination + workbook resolution. Three server PRs landed together as v2.19.3 (#61, #63, #65).
  • 2026-06-04 (morning) β€” EE-5 lax decode + workload + input alias. v2.19.1 + v2.19.2 β€” unblocked Rust worker β†’ Rust server emission + canonical v10 playbook compatibility.
  • 2026-06-04 (early morning) β€” Phase F R4-5 + R4 complete. N=2 shard kind validation script + ExecutionService refactor.
  • 2026-06-03 β€” Phase F R4 series β€” DbPoolMap N+1 pool layer, AppState wiring, per-execution handler cutover, cluster-wide list fan-out.
  • 2026-06-02 (afternoon) β€” Architecture pivot: rest of migration moves to system playbooks. Closed #30, #45; promoted #46.

Releases

See Releases for the per-repo release log with links to GitHub Releases pages.

Recent:

  • 2026-10-04 β€” noetl/noetl pypi-v5.1.1 β€” πŸ”’ the shipped wheel's RustSec count goes 5 β†’ 3. h2 0.4.19 and rustls 0.23.45 clear two advisories Dependabot reported as zero; event-listener 5.4.2 and rustls-webpki 0.103.15 ride along. Verified on the published artifact (4 files sha256-matched, sdist Cargo.lock read, 5.1.0's lock scanned first as a positive control). Remaining 3 are documented deferrals (#402).
  • 2026-10-04 β€” noetl/noetl pypi-v5.1.0 β€” πŸ“¦ the artifact finally matches the lock β€” the 7 cleared Rust advisories reach the wheel users install (#395). ⚠⚠ Verified by upload counts, because twine --skip-existing makes a publish-nothing run go green.
  • 2026-10-04 β€” noetl/cli v5.0.3 β€” πŸ“¦ arm64 debs are built and indexed; the apt Release file gains its 8 identifying fields and stops listing itself (#390).
  • 2026-09-04 β€” noetl/server v3.101.0 β€” πŸ”§ scoped, inspect-only tier repair endpoint (server#399). Re-mirrors only the event IDs the comparator reports absent; both comparisons under ParityRecording::Inspect so a repair does not inflate the counter its own alert reads. Inert, not run against prod (prod stays v3.100.4).
  • 2026-09-01 β€” noetl/worker v5.129.1 β€” πŸ”₯ bounded manifest retention + ingest failure counters, the two fixes for the 8-hour silent dispatch outage. Deployed: manifest 871 β†’ 169 MB.
  • 2026-08-29 β€” noetl/server v3.92.0 β†’ v3.94.0, noetl/worker v5.124.0, noetl/cli v4.22.0 β€” πŸ“ the catalog gets its own log tier, relation and bulk loader; #307's projection fold becomes measurable.
  • 2026-08-11 β€” noetl/worker v5.115.3 β€” ⏱️ the cooldown gates RETRY, never BELIEF. is_reachable() consulted the cached-negative deadline, so a lapsed window re-promoted a dead tier with no evidence; belief moved to a flag only a real successful op sets. Mutation-checked twice (worker#262, #257). Inert, not deployed.
  • 2026-08-05 β€” noetl/server v3.64.0 β†’ v3.69.0, noetl/worker v5.98.0 β†’ v5.99.1, noetl/gateway v3.8.0 β€” πŸ“Š the metrics surface becomes readable when nothing has happened: *_build_info{version} in all three binaries, publish-skip reasons and all seven sweep outcomes pinned at 0, and the dead NATS + JetStream metrics deleted. Prod untouched.
  • 2026-08-03 β€” noetl/worker v5.92.0 β€” πŸ›‘οΈ the writer's SIGTERM seal now covers the events log, and ingest is actually stopped first. worker#211 (ddb4de8): fail-loud internal bus source (closes #221), sequenced seal + confirmed stop-ingest (#209 graceful half), face supervision. The release gate caught that the first seal attempt never actually closed the listener β€” Notify::notify_waiters() stores no permit and the acceptor registers on its first poll, so an early shutdown lost the signal (24/30 runs under contention; lsof showed the writer's own acceptor still LISTENing). Fixed with a watch + an ack after the fd is dropped. Deployed to shastaratech prod by digest @sha256:ad1e96063bf5…81725; a controlled SIGTERM sealed both hosts in 568 ms with every cursor resuming clamped=false. ⚠️ semantic-release then pushed 5.91.3 over it on main (#224).
  • 2026-07-30 β€” noetl/server v3.58.3 + noetl/worker v5.81.3 β€” 🏁 the EHDB command bus goes faster than NATS, and learns to survive a writer restart. #205: group commit + off-lock fsync in the writer (ehdb#301 03e94be) + pipelined publish (server#289, worker#195) β€” prod dispatch p50 138.5 / p99 181.0 ms vs the 338 ms NATS baseline (~2.4Γ—). #208: keepalive + negotiated coordinator heartbeat, durable committed cursor clamped to the reopened tip, seal on SIGTERM (ehdb#302 86a24f9, worker#196, server#290) β€” writer deleted mid-load β†’ 38/38, 0 dup / 0 loss, ~2.7 s redial. These are the images now running the production command bus.
  • 2026-07-28 β€” noetl/worker v5.81.1 + noetl/server v3.58.1 β€” πŸ› the #203 feed-delivery fix: the command-bus writer now assigns the feed ordering key (append_writer_assigned) instead of trusting the producer's sparse snowflake id, so an out-of-order append can no longer land below the follower cursor and be silently dropped (ehdb#300 β†’ ehdb d4b6235; worker#193 + server#287). Kind before/after on an identical 40-concurrent burst: 17/40 β†’ 41/41. These are the images now running the EHDB command bus in production.
  • 2026-07-26 β€” noetl/worker v5.81.0 β€” πŸͺ #199 Slice C auto-sink observe-only (worker#192): the auto-sink loop observes and writes nothing; the write slice is held for design sign-off. Default-off.
  • 2026-07-26 β€” noetl/worker v5.80.0 β€” πŸͺ #199 Slice A sink-confirmation signal (worker#190): an author-declared sink: true connector step marks its execution pending-sink on dispatch and confirm_sunks on success. Author-declared, never inferred (a postgres/http/transfer step can be a read β€” classifying by kind would confirm-sink on reads and release un-sunk context); fail-safe is to under-confirm. Default-off (NOETL_SINK_GATE_EVICTION).
  • 2026-07-25 β€” noetl/worker v5.79.0 β€” πŸͺ #199 Slice B durable-segment GC gate (worker#191): the reclaim pass consults SharedWalIndex::any_sink_pending() and defers while any execution is pending-sink. Behavior-neutral, default-off.
  • 2026-07-24 β€” noetl/cli v4.21.0 β€” 🐍 Stage 2 of the PyPI wheel: the duckdb crate (features = ["bundled"]) gated behind the duckdb-integration feature so the wheel matrix builds without compiling the DuckDB C++ amalgamation (cli#74). Distributed artifacts (tarballs, deb, Homebrew, Dockerfile) keep the engine explicitly. This is the CLI crate compiled into the published noetl 5.0.0 wheel (noetl --version β†’ 4.21.0).
  • 2026-07-24 β€” noetl/cli v4.20.0 β€” 🐍 Stage 1 library extraction (cli#73): src/main.rs becomes a shim over src/lib.rs exposing cli_main(argv) -> i32 (the full 24-subcommand tree) + re-exported PlaybookRunner/RunOutcome. Verified byte-identical to 4.19.0. This is what the wheel links, not a reimplementation.
  • 2026-07-26 β€” noetl/noetl 5.0.0 on PyPI β€” 🐍 the NoETL CLI shipped as a Python wheel, the Rust CLI bound via PyO3 (abi3-py39): noetl console script + import noetl over the real PlaybookRunner. 4 artifacts (macOS arm64, Linux x86_64/aarch64, sdist); Windows dropped (the CLI depends on nix/POSIX), macOS-Intel covered by the sdist. https://pypi.org/project/noetl/5.0.0/ (noetl#692, tag pypi-v5.0.0).
  • 2026-07-23 β€” noetl/server v3.58.0 + v3.57.0 β€” 🧱 EHDB write-behind-cache boundary: command-input tiering (server#284) and permanent-log-lean (server#283) β€” an over-floor inline step result / command input is stripped out of the permanent log at the events_project materializer into a byte-source reference. Both default-off (NOETL_PERMANENT_LOG_LEAN).
  • 2026-07-14 β€” noetl/tools v3.25.0 + noetl/cli v4.17.0 β€” 🧩 provider tool round 5: the noetl provider {plan,drift,orphans,adopt} CLI verbs (Terraform-style CLI over the tool, local mode; glue over ProviderTool::execute+provider_state+to_tools_config) (cli#68, a2e5b4d) + the query-tier fetch adapter extract_facts_from_tier_records (unwrap the payload string + recursive fact search β€” fixes the round-3/4 fixed-path bug where the fold silently returned nothing against real event data on 3.23/3.24) + stack-scoped detect_orphans_scoped/detect_stack_conflicts (tools#90, 5a06c7e). noetl-executor unchanged at 0.7 (zero executor/ files touched). crates.io tarball-verified + released-binary proof (noetl-v4.17.0-darwin-arm64, all 4 verbs GET-only zero-writes). Follow-up 3 (first-class event type) DEFERRED. ai-meta toolsβ†’5a06c7e + cliβ†’a2e5b4d; #189 OPEN. Mocks only β€” no prod/kind/live-cloud.
  • 2026-07-13 β€” noetl/tools v3.24.0 + noetl/cli v4.16.0 (+ noetl-executor 0.7.0) β€” 🧩 provider tool round 4: reconcile policy (report new-default / enforce / adopt) + confirm-gated drift adoption (adopt = accept live actual as new desired, read/GET-only, blind+stale REFUSED; covers update-in-place + import) (tools#89, c172efb), AND local-mode reconcile/known_desired passthrough + executor 0.6.0β†’0.7.0 (cli#66, 7557dde). All three crates crates.io-verified. Behavior change (user-confirmed): converging needs reconcile:enforce. ai-meta toolsβ†’c172efb (c6790983) + cliβ†’7557dde (e4d3a32c); #189 OPEN.
  • 2026-07-13 β€” noetl/cli v4.15.1 β€” πŸ› οΈ release-cli crate-publish FIXED: bumped noetl-executor 0.5.0β†’0.6.0 (+ bin dep "0.5"β†’"0.6") so the Tool::Provider variant ships to crates.io. The publish-crate job had failed on v4.14.0 + v4.15.0 (crate channel stuck at 4.13.0); full release run 29290603328 GREEN β€” executor 0.6.0 + noetl 4.15.1 now live on crates.io, binaries + .deb on the v4.15.1 release. apt + homebrew still SKIP (missing APT_REPO_PUSH_TOKEN / HOMEBREW_TAP_PUSH_TOKEN) but now emit a ::warning::. cli#67, ai-meta cliβ†’5bb1705 (7d58e7d); #190 OPEN pending the two repo secrets.
  • 2026-07-13 β€” noetl/tools v3.23.0 + noetl/cli v4.15.0 β€” πŸš€ provider tool round 3: confirm-gated destroy (*.delete/services.disable/*.iam.remove_binding β€” live-resolved plan + SHA-256 plan_digest; blind apply + stale digest REFUSED; no wildcard) + EHDB ownership projection (secret-free provider_fact + pure provider_state fold over the raw eventlog tier, not the secret-free /api/ehdb/events read-model) + config-level endpoint override (tools#88, aa32938), AND local-mode endpoint/stack/confirm passthrough + re-pin to noetl-tools 3.23.0 (cli#65, 94cc72a; local-mode proof green on 3.23.0). ai-meta toolsβ†’aa32938 (f6eeb479) + cliβ†’94cc72a (cbc7be99); #189 OPEN, round-4 = drift adoption. Mocks/wiremock only β€” no prod/kind/live-cloud, no real destroys.
  • 2026-07-13 β€” noetl/tools v3.22.0 + noetl/cli v4.14.0 β€” πŸš€ provider tool round 2: apply-mode idempotent GET-first converge for the 4 ensure/ensure_binding actions (+ services.enable/projects.link, re-run = no-op) + bounded inline LRO polling (tools#87, eb3d8c7), AND kind: provider dispatches through noetl exec --runtime local β€” same tool the worker runs, re-pinned to noetl-tools 3.22.0 (cli#64, 2d46013; supersedes cli#63, closed). ai-meta toolsβ†’eb3d8c7 (6cd1ba38) + cliβ†’2d46013 (32a8fb86); #189 OPEN, both design forks DECIDED for round 3.
  • 2026-07-13 β€” noetl/tools v3.21.0 β€” πŸ†• new kind: provider cloud provider tool (Google REST MVP; Cloud Resource Manager v3 / Cloud Billing v1 / Service Usage v1 over REST + GcpAuth ADC, zero new heavy deps; plan/apply + explicit-auth-for-mutations) (tools#86, cd9d71c; ai-meta e9b3e14e; #189 OPEN)
  • 2026-07-04 β€” noetl/server v3.52.0 β€” πŸ” #169 Auth0 JWT RS256/JWKS signature verification (server#276) shipped + deployed to prod in shadow (NOETL_AUTH_VERIFY_SIGNATURE, default off; also carries #166 Phase 5 dark). Prod v3.50.0β†’v3.52.0.
  • 2026-06-29 β€” noetl/server v3.48.0 + noetl/worker v5.48.0 β€” ⚑ #156 off-server drive tail-attach (server#268 ddca5c4 + worker#142 12710b6): server attaches the per-execution event tail to the __offserver_build__ dispatch, worker applies it to its WAL index before building β†’ per-hop O(execution) not O(global log), removing the drain-lag 8s reconcile cliff. Flag NOETL_OFFSERVER_ATTACH_TAIL (default off). DEPLOYED on prod flag-off (behavior-neutral) then flipped on; 5 Muno turns COMPLETED identical correct widgets, 90 drives attached O(tail), 0 restarts, reconnect 0/30s; 1-step rollback. ai-meta pointer f2b5666
  • 2026-06-28 β€” noetl/worker v5.47.3 β€” πŸš‘ #157 hotels-turn drive wedge fix (worker#141): __orchestrate__ drive result stays inline regardless of size, so a render-hop result over the 100KB budget no longer offloads to a result-store ref the server can't resolve under NOETL_RESULT_STORE_DUAL_WRITE=false (#104) β€” Muno hotels turn now COMPLETES + returns a hotel_list widget; deployed both pools (0 restarts), ai-meta pointer 60938f4
  • 2026-06-27 β€” noetl/server v3.47.0 β€” πŸš€ #104 result_store dual-write retirement gate (NOETL_RESULT_STORE_DUAL_WRITE, default on = behavior-neutral) (server#267); flipped OFF on prod β€” result_store writes retired, #104 tier authoritative, data frozen
  • 2026-06-26 β€” noetl/tools v3.19.0 β€” πŸ†• G1 container GPU placement (node_selector/tolerations/volumes) + G2 poll-to-terminal helper (tools#82); SLM platform foundation #144/#145, review-only
  • 2026-06-26 β€” noetl/worker v5.47.0 β€” πŸ†• G2 watcher-free poll completion fallback (NOETL_CONTAINER_COMPLETION_POLL, default off) (worker#139); #145, review-only
  • 2026-06-26 β€” noetl/server v3.46.0 β€” πŸ†• G3 versioned model/dataset/eval registry (NOETL_REGISTRY_ENABLED, default off) (server#266); #146, review-only
  • 2026-06-23 β€” noetl/worker v5.46.0 β€” πŸͺΆ #104 OQ5 Option A: producer-staged result tier (NOETL_RESULT_PRODUCER_STAGE, default off) β€” decouples the over-budget tier write from result_store; materializer skip-on-exists.
  • 2026-06-23 β€” noetl/server v3.45.0 β€” πŸš€ #104: WI/ADC GCS auth (3-mode none/static/adc) MERGED + ROLLED TO PROD as WI KSA noetl-server-rust (@sha256:d3cbf1ad…); result-tier GCS ENV applied, all tier-enable flags OFF β†’ tier inert; off-server cutover stayed healthy, 0 restarts (server#265, fad5d8a).
  • 2026-06-23 β€” noetl/server v3.44.0 β€” πŸͺΆ #104 Phase F: result-tier GC sweeper (NOETL_RESULT_TIER_GC, default off) β€” conservative dry-run-first, never deletes a live-referenced object (server#264, 341b614).
  • 2026-06-23 β€” noetl/worker v5.45.0 β€” πŸͺΆ #104 Phase F: result-tier DR re-derive (NOETL_RESULT_TIER_DR, default off) β€” materializer verify-and-repair, byte-identical rebuild of a missing/corrupt tier object (worker#131, dd07016).
  • 2026-06-23 β€” noetl/worker v5.44.0 β€” πŸͺΆ #104 Phase E: side-effect durability barrier (NOETL_SIDE_EFFECT_BARRIER, default off) β€” adopt-only, side effects fire exactly once across re-drive; depends on published noetl-tools 3.17 (worker#130, d696f7e).
  • 2026-06-23 β€” noetl/tools v3.17.0 β€” πŸͺΆ #104 Phase E: registry::kind_is_side_effecting side-effect classifier (conservative default true; only noop/rhai false) (tools#78, 1d49dd5; members noetl-directives + noetl-locator re-published first).
  • 2026-06-23 β€” noetl/server v3.43.0 β€” πŸͺΆ #104 Phase D: mint-authoritative flag (NOETL_RESULT_MINT_AUTHORITATIVE, default off) + result_store dual-write counter (noetl_result_store_dual_write_total) β€” the reversible dual-write fallback leg of the minting flip; no Cargo.toml change (resolves noetl-locator 0.1.1 from the registry) (server#263, 6f6b9ef).
  • 2026-06-23 β€” noetl/worker v5.43.0 β€” πŸͺΆ #104 Phase D: the minting flip β€” NOETL_RESULT_MINT_AUTHORITATIVE (default off) makes the materializer the authoritative tier writer + resolve-by-URN the primary consume path, with the dual-written result_store as the fail-safe fallback; new noetl_worker_result_mint_authoritative_total{path} (tier | legacy_fallback) (worker#129, be6863a).
  • 2026-06-23 β€” noetl/server v3.42.0 β€” πŸͺΆ #104 Phase C: GCS object backend + cell-endpoint registry + GET /api/internal/cells (resolve-by-URN read side; default-off) (server#262, c2d5ca9).
  • 2026-06-23 β€” noetl/worker v5.42.0 β€” πŸͺΆ #104 Phase C: resolve-by-URN read path + fixes B/B1 (references-in-state behavior, flatten_single_tool_result, closes OQ6; adds published arrow=53, resolves noetl-tools 3.16.0; 38 unit tests) (worker#128, 7971041).
  • 2026-06-22 β€” noetl/tools v3.16.0 + noetl-locator v0.1.1 β€” πŸͺΆ #104 Phase B: ResultCoordinates::parse/from_locator (the inverse of logical_uri); additive β†’ member bumped 0.1.0β†’0.1.1, published (tools#77).
  • 2026-06-22 β€” noetl/server v3.41.0 β€” πŸͺΆ #104 Phase B: ensure the sibling noetl_result_materializer durable consumer at stream-birth (own ack cursor) (server#261).
  • 2026-06-22 β€” noetl/worker v5.41.0 β€” πŸͺΆ #104 Phase B: shadow Feather result tier β€” separate noetl_events consume-loop writes over-budget results (tabular β†’ Feather, non-tabular β†’ JSON) at the derived Β§7 key; gated NOETL_RESULT_MATERIALIZER_ENABLED (default off) (worker#127).
  • 2026-06-22 β€” noetl/server v3.40.0 β€” πŸ“¦ #104 Phase A: accept the canonical result URI behind NOETL_RESULT_URI_ACCEPT (default off), parsed via the slim noetl-locator 0.1.0 (heavy graph absent from the control plane) (server#260, c89d078).
  • 2026-06-22 β€” noetl/tools v3.15.0 + new crate noetl-locator v0.1.0 β€” πŸ“¦ #104 Phase A: extract the slim dependency-free noetl-locator crate (pure std; ResourceLocator/ResultCoordinates/shard_key/CellPlacement/legacy parse) so the control-plane server parses the result URI without noetl-tools' heavy graph; noetl-tools re-exports it as noetl_tools::locator (tools#76, dc0c5d8; noetl-locator 0.1.0 published to crates.io).
  • 2026-06-22 β€” noetl/worker v5.40.5 β€” βœ… #95: adopt noetl-tools 3.14.2 (postgres temporal/identity serialization); Cargo.lock pin 3.14.1β†’3.14.2, no worker source change; built noetl-worker-rust:v5.40.5 @sha256:45212dbe… + rolled by digest to prod under the live off-server cutover (worker#126, da24952).
  • 2026-06-22 β€” noetl/worker v5.40.4 β€” βœ… #127: adopt noetl-tools 3.14.1 (task_sequence per-sub-task context optimization); Cargo.lock pin 3.14β†’3.14.1, no worker source change; built noetl-worker-rust:0afbf5c + rolled to prod under the live off-server cutover (worker#125, 0afbf5c).
  • 2026-06-22 β€” noetl/tools v3.14.2 β€” βœ… #95: postgres pg_value_to_json now serializes tz-naive timestamp (NaiveDateTime) + date/time/uuid/numeric (lossless binary-wire decode)/bytea instead of returning null; the auth0_login expires_at: null repro (tools#75, 6d9b674).
  • 2026-06-22 β€” noetl/tools v3.14.1 β€” βœ… #127: behavior-preserving task_sequence per-sub-task context reuse β€” render_value builds the proxied minijinja context once + build_context_with_overlay skips the redundant to_template_context() clone; micro-bench βˆ’61.6%/2.6Γ— per-sub-task context cost (tools#74, c8656c1).
  • 2026-06-22 β€” noetl/server v3.39.6 β€” βœ… #123: surface a non-iterable loop in: as a terminal playbook.failed β€” the off-server drive decodes the {"error":…} envelope (decode_orchestrate_error, metric noetl_orchestrate_drive_total{stage="drive_error"}) instead of silently wedging (commands=0); orchestrate-core prefixes the offending step name onto the evaluate_loop error (server#258, 7f109a9, squash 275b914). Code-only (worker stays v5.40.3); 600 server + 135 orchestrate-core tests + clippy; kind-validated prod-exact (absent iterable β†’ FAILED with a clear message, valid [1,2,3] still COMPLETED).
  • 2026-06-22 β€” noetl/server v3.39.5 β€” βœ… #121 second-half: gate BOTH off-server-drive sites in trigger_orchestrator_inner on should_publish so system/* execs drive server-built run_state (they INSERT to noetl.event, never enter the WAL); regular execs keep the off-server path (server#257, c421273, squash 54ac277). Server-only (worker stays v5.40.3); 612 tests + clippy; kind full-gate system/scheduled_cleanup BEFORE 9Γ— WAL chain incomplete β†’ AFTER COMPLETED 0 loops.
  • 2026-06-21 β€” noetl/server v3.39.4 β€” βœ… #121 first-half (partial): link gate-off command.claimed through ChainHeads + don't route system/ execs to the off-server WAL drive (server#256, 77aaa06)
  • 2026-06-21 β€” noetl/worker v5.40.3 β€” βœ… #125 + #126 adopt noetl-tools 3.14.0 (task_sequence control flow + http body data-shape fixes); 10Γ—1000 batch pft_flow_test clean (worker#124, 6dd3449).
  • 2026-06-21 β€” noetl/tools v3.14.0 β€” βœ… #125 task_sequence honours do: jump/break/retry (tools#73, 638c3c6).
  • 2026-06-21 β€” noetl/tools v3.13.1 β€” βœ… #126 http tool body under data (Python-era contract restored; back-compat body alias) (tools#72, 8dd0e1f).
  • 2026-06-21 β€” noetl/server v3.39.3 β€” βœ… #124 distributed task_sequence forward set:/sibling bindings no longer render empty; defer sub-task templates with unresolved vars to the worker re-render (server#255, 365d3be).
  • 2026-06-21 β€” noetl/server v3.39.2 β€” βœ… #120 reduce barrier no longer deadlocks on open/asymmetric loop joins; runtime liveness filter in the orchestrate-core barrier (server#254 28e8950)
  • 2026-06-20 β€” noetl/server v3.39.0 β€” βœ… #116 program-scale step 2: execution-affinity single-owner WRITE ORDERING (multi-replica gate-ON validated) β€” closes the chain-fork race step 1 left open. Affinity routes every trigger for an execution to the replica that ShardConfig::owns it (non-owner forwards a reverse-proxy POST); owner's single-process drive lock + in-memory ChainHeads make the readβ†’advance atomic, no distributed lock; KV = genesis/handoff vehicle (kv_remote_hitβ†’0). src/affinity.rs; NOETL_EXECUTION_AFFINITY/NOETL_PEER_URL_TEMPLATE/NOETL_SHARD_INDEX_FROM_HOSTNAME (all default off); metric noetl_execution_affinity_total{outcome} (server#252 5e00d0a). 2-replica gate-ON kind PASS: chains roots=1/dangling=0/walk==total (NO fork), forwarded_ok +9, never-scan + sole-writer across replicas. Follow-up #117 (off-server from_events spine event_id-order wedges fan-in under inversion).
  • 2026-06-20 β€” noetl/server v3.38.0 β€” βœ… #115 program-scale step 1: multi-replica coherence DATA LAYER β€” NOETL_REPLICA_COHERENCE=nats_kv (default local, prod unchanged) backs ChainHeads + ExecDescriptor with JetStream KV buckets (head CAS + descriptor CAS merge); src/coherence.rs; metric noetl_replica_coherence_total{structure,op,outcome} (server#251 8f39a79). Kind: single-replica parity with local; 2-replica cross-replica resolves proven. Necessary-not-sufficient β†’ execution-affinity STAGED (2+ replicas still fork the chain).
  • 2026-06-20 β€” noetl/server v3.37.0 β€” βœ… #115 Phase 5 atomic-working-item context (tenet 6): input_binding + NOETL_ATOMIC_ITEM_CONTEXT (default off) (server#250 a96ade8)
  • 2026-06-20 β€” noetl/worker v5.40.0 β€” βœ… #115 Phase 5 forward the atomic-item-context flag onto the off-server from_events drive (worker#121 2484d17)
  • 2026-06-20 β€” noetl/server v3.36.0 β€” βœ… #115 Phase 6 retire the hot-path noetl.event read class; the table is AUDIT-ONLY (server#249, b71ca1d): NOETL_EVENT_READ_PATH=event_scan|audit_only (default event_scan, prod unchanged). Under audit_only the remaining lifecycle readers (get_catalog_id, inherit_parent_trace, dedup-audit + container-callback catalog/existence) serve from the in-memory ExecDescriptor; cold β†’ noetl.command (synchronous queue) β€” never a noetl.event scan. Proof metric noetl_event_hotpath_reads_total{site,outcome}. Gate-ON kind-validated: hot-path scan Ξ”0 + drive state_build_total/event_scans Ξ”0 β‡’ ZERO noetl.event scans on the hot path, end-to-end; audit/replay still work; 585 tests + clippy green. RFC never-scan end state (tenet 3) reached under the flag.
  • 2026-06-19 β€” noetl/worker v5.37.0 β€” βœ… #115 Phase 4 off-server state-builder kernel + WAL shadow loop (worker#118, fef961c): pool-side per-execution chain index sourced from the noetl_events WAL; chain_walk() headβ†’root spine in event_id order (parity by construction); cache keyed by the immutable chain head (CacheHit / Incremental tail-advance / ColdRebuild). Live WAL shadow loop (NOETL_STATE_BUILDER_SHADOW, default off) + metrics. Gate-ON kind-validated (993 WAL events, 0 noetl.event scans, 28 cold + 21 incremental); 8 unit tests + clippy green; default off. Drive cutover staged.
  • 2026-06-19 β€” noetl/server v3.33.0 β€” βœ… #115 Phase 4 NOETL_STATE_BUILDER=offserver|server flag scaffold (server#246, 3e6006d, default server): the server-side flag for the off-server state-builder drive cutover (staged). 2 config tests; no prod default changed.
  • 2026-06-19 β€” noetl/server v3.32.0 β€” βœ… #115 Phase 3 chain-walk state builder (flagged, default-off) (server#245, ai-meta pointer bumped): behind NOETL_STATE_BUILD_MODE=chain_walk the drive reconstructs WorkflowState by following the one-level prev_event_id chain headβ†’root (in-memory ChainHeads head + (execution_id,event_id) PK lookups β€” never a WHERE execution_id scan) β†’ same from_events (parity by construction); event-scan kept as the default + fallback (cold-head / lag / non-genesis). NOETL_STATE_BUILD_PARITY_CHECK shadow-builds both ways in one REPEATABLE READ snapshot. New metrics noetl_state_build_total{mode,outcome} / _event_scans_total (no-scan proof) / _chain_hops / _parity_total{result}. Gate-ON kind-validated: parity 41/41 MATCH, scans=0 / 1064 hops / 0 fallbacks, all topologies COMPLETE, sole-writer + lag-0, 577 tests + clippy green. No prod default changed.
  • 2026-06-19 β€” noetl/server v3.31.0 β€” βœ… #115 Phase 2 one-level prev_event_id event chain (server#244, ai-meta afdb365): each noetl.event/noetl.command carries the chain link, stamped at the emit chokepoint from a per-execution chain-head watermark (ChainHeads), covering both gate paths + the materializer. Additive; nothing reads it yet (Phase 3). Kind-proven walkable/1-root/no-gap/no-scan across 6 gate-ON topologies; 573 tests + clippy green. Companion DDL noetl/noetl ecd16a2 (noetl#667). No prod default changed.
  • 2026-06-19 β€” noetl/server v3.30.0 β€” βœ… #115 Phase 1 surface _ref/_store on kept refs + refs_in_state default true (server#243): consume-side accessors for {{ step._ref }} lazy-load + storage-tier predicates; references stay out of state/commands by default (worker selective-resolve landed in worker#117 v5.36.0). Closed #113 + #114; kind gate-ON all 9 stalls COMPLETE.
  • 2026-06-19 β€” noetl/server v3.29.4 β€” 🐞 #113 off-server drive: recover offloaded drive result + stop drive on cancel (server#241, rig e2e#63). apply_worker_orchestration resolves+decodes an offloaded __orchestrate__ result (over the 100KB inline budget β†’ durable reference.ref) instead of dropping it β†’ non-convergent re-loop (metric ref_resolved); cancel now matches underscore playbook_cancelled + a terminal guard evicts the orch-cache (no restart). Kind gate-ON proven; 5/9 #113 fixtures COMPLETE, the other 4 hit a distinct oversized-command.issued stall β†’ #114. No prod default flipped.
  • 2026-06-19 β€” noetl/server v3.29.5 β€” 🐞 #114 offload oversized command context: a command.issued context over NOETL_COMMAND_CONTEXT_MAX_BYTES (512KB) is offloaded to noetl.result_store with a {__context_ref__} marker, resolved in get_command/claim_command (server#242, rig e2e#64) β€” published events stay under NATS max_payload, no publish-wall wedge. Kind gate-ON: rig PASS, all command.issued <1MB, 6 of #113's 9 fixtures COMPLETE; remaining 3 + cutover gated on the refs_in_state consume side (#101). No prod default changed.
  • 2026-06-19 β€” noetl/server v3.29.4 β€” 🐞 #113 recover offloaded drive result + stop drive on cancel: apply_worker_orchestration resolves+decodes an over-budget __orchestrate__ drive result via result_store.resolve (metric ref_resolved) instead of dropping it β†’ non-convergent re-loop; cancel matches underscore playbook_cancelled + a terminal guard evicts the orch-cache (server#241, rig e2e#63). No prod default changed.
  • 2026-06-19 β€” noetl/server v3.29.3 β€” 🎯 #103 cutover COMPLETE, FLIP-READY: the 2 ExecutionService cancel/finalize writers route through the emit_event chokepoint (server#240, e2e rig e2e#62) β€” the last synchronous server noetl.event writer under the gate is closed. Kind-proven both modes (gate-off byte-identical INSERT; gate-on PUBLISHED + materializer sole writer + terminal state + 0 loss/dup). All three flip blockers closed β†’ PUBLISH_ONLY flip is a staged operator decision. Default-off; no prod default changed.
  • 2026-06-19 β€” noetl/server v3.29.2 β€” off-server-drive Γ— gate crash-recovery: cold-cache apply rebuilds WorkflowState from the durable log instead of dropping the in-flight drive result (server#238, refs #104/#103). Unblocks the PUBLISH_ONLY flip (off-server drive Γ— gate now kind-proven). Confined to the cold branch; no prod default changed.
  • 2026-06-19 β€” noetl/tools v3.13.0 + noetl/worker v5.34.0 β€” #103 ack-after-materialize durability: deferred ack-after-processing capability (tools#71: AckMode::Defer + $JS.ACK durable handles + ack/nack/term) + in-process CQRS materializer consume-loop (worker#115: drainβ†’projectβ†’ack-only-on-success, redeliver on failure) + system-pool wiring (ops#194). Kind fault-injection: gate-on sole-writer loss=0 across a mid-drain failure. Default-off.
  • 2026-06-18 β€” noetl/server v3.28.0 β€” worker-driven orchestrator drive now default ON (NOETL_ORCHESTRATE_PLUGIN_DRIVE defaults true) (server#233, closes #108; scale-soak-gated, revert = =false).
  • 2026-06-18 β€” noetl/worker v5.33.0 β€” pool-affinity decline (drive isolated on the system pool) (worker#114, refs #108 (b)).
  • 2026-06-14 β€” noetl/worker v5.22.0 β€” transfer endpoint credential-alias resolution, both Snowflake↔Postgres directions (worker#87, closes #99).
  • 2026-06-14 β€” noetl/tools v3.10.0 β€” Snowflake↔Postgres transfer arms + flatten credential config (tools#65, closes #99).
  • 2026-06-14 β€” noetl/tools v3.9.2 β€” Snowflake SQL-API context in request body + multi-statement split (tools#64, refs #98).
  • 2026-06-14 β€” noetl/tools v3.9.1 β€” set User-Agent on the Snowflake HTTP client (tools#63).
  • 2026-06-14 β€” noetl/tools v3.9.0 β€” Snowflake key-pair JWT authentication (tools#62; kind-validated on live sf_test account).
  • 2026-06-12 β€” noetl/server v3.5.0 β€” POST /api/execute/batch
    • opt-in exactly-once dedup window (server#189, RFC #90 Phase 7 β€” scale hardening).
  • 2026-06-12 β€” noetl/worker v5.19.0 β€” batch dispatch + dedup opt-in + per-subscription rate limits (worker#79, RFC #90 Phase 7 β€” scale hardening, closes #90).
  • 2026-06-12 β€” noetl/cli v4.11.0 β€” noetl subscribe, local-mode subscription listener (cli#60, closes cli#59, RFC #90 Phase 6). Standalone kind: Subscription listener + FileEventSink JSONL trail + local_disk store-and-forward spool; cli-only (reuses noetl-tools v3.5.0 source+spool). ai-meta β†’ cli 2fb3fb0.
  • 2026-06-11 β€” noetl/server v3.0.6 β€” round-trip JSON null in whole-object {{ step }} references (server#177, closes noetl/ai-meta#89). A null field in a {{ step }} envelope rendered as the JS token undefined (invalid JSON), so the consuming step received an unparseable str; render_to_value now retries with | tojson (undefined/none β†’ JSON null) β€” the server renderer had diverged from the noetl-tools engine that already did this. Kind-validated: cursor pagination collects all 35 events through the terminal next_cursor: null page. ai-meta pointer β†’ server 8e17fbe.
  • 2026-06-10 β€” noetl/tools v3.1.1 β€” multi-tool sibling references (tools#48, closes noetl/ai-meta#87). TaskSequenceTool now injects each sub-tool's result under its label so a later sub-tool resolves {{ <label>.<field> }} (was rendering empty β€” a syntax error at or near "," in unquoted numeric SQL positions). Worker adopts via worker#69. Kind-validated (save_edge_cases test_large_payload β†’ record_count = 100). ai-meta pointer β†’ tools 76f942a + worker b97f642.
  • 2026-06-10 β€” noetl/server v3.0.3 β€” container-callback insert matches the deployed noetl.event schema (server#173, tracks noetl/ai-meta#43). The handler's call.done insert targeted a non-existent attempt column β†’ HTTP 500 on every watcher callback; replaced with an inline INSERT matching the working ingestion path. Unblocked the container-callback chain (kind-val GREEN both probes). ai-meta pointer β†’ 5d2cf58.
  • 2026-06-10 β€” noetl/gui v1.11.0 + v1.11.1 β€” credential View/Edit recovery for pre-wallet records (gui#36, closes noetl/ai-meta#82) + dev:kind convenience script (gui#35). ai-meta pointer β†’ 8cacc9e.
  • 2026-06-10 β€” noetl/server v3.0.2 β€” container-tool command type contradiction fix (server#172, closes noetl/ai-meta#81). ToolSpec.command Option<String> β†’ Option<serde_json::Value>: the container tool's array command now decodes server-side + passes through to the worker's Vec<String>; scalars stay JSON strings for shell/db tools. Kind-val GREEN (K8s Job reaches Complete 1/1).
  • 2026-06-08 β€” noetl/tools v2.24.2 β€” clippy cleanup: 15 warnings resolved across 7 files (tools#44, closes tools#42). Mechanical lint fixes, zero behavioral changes.
  • 2026-06-05 β€” noetl/tools v2.18.4 β€” duckdb command: alias (parity with postgres) + http params/headers/form non-string coercion (tools#26); worker adopts it (worker#51). Unblocks the pagination + http + duckdb-command fixtures.
  • 2026-06-05 β€” noetl/tools v2.18.3 β€” postgres command: alias
    • multi-statement SQL on postgres + duckdb (tools#24, closes tools#23); worker adopts it (worker#50). duckdb_test + json_serialization_save GREEN.
  • 2026-06-05 β€” noetl/tools v2.18.2 β€” postgres tool surfaces the real SQLSTATE + message instead of db error (tools#21); worker bumped to it (worker#49).
  • 2026-06-05 β€” noetl/server v2.22.0 β€” Secrets Wallet Phase 2: GCP Cloud KMS KeyManager (Cloud KMS :encrypt/:decrypt + Workload Identity); runtime NOETL_KMS_PROVIDER (local/gcp-kms); KEK can leave the process (server#81, tracks #61). Kind-validated on local.
  • 2026-06-05 β€” noetl/server v2.21.0 β€” Secrets Wallet Phase 1c/1d: credentials + keychain store envelope-encrypted (per-record DEK wrapped by the KEK); self-describing {"v":1,…} blob, forward-only (server#79, tracks #61). Kind-validated end-to-end.
  • 2026-06-05 β€” noetl/server v2.20.0 β€” Secrets Wallet Phase 1b: envelope-encryption core β€” KeyManager/LocalDevKms/EnvelopeCipher (server#77).
  • 2026-06-05 β€” noetl/server v2.19.8 β€” Secrets Wallet Phase 1a: remove the all-zeros default encryption key, fail closed (server#75, tracks #61). Kind-validated.
  • 2026-06-05 β€” noetl/tools v2.18.5 β€” dollar-quote-aware statement splitter; the 2.18.3 splitter shredded plpgsql $$ … $$ blocks (tools#27).
  • 2026-06-05 β€” noetl/server v2.19.7 β€” defer task_sequence _prev/_results refs at command-build (server#73); nested-pipeline templates render at runtime β†’ iterator_save_test GREEN.
  • 2026-06-05 β€” noetl/worker v5.11.3 β€” resolve keychain aliases on task_sequence sub-tasks (worker#48); nested postgres-in-pipeline steps connect.
  • 2026-06-05 β€” noetl/server v2.19.6 β€” credential store base64-armors the AES-GCM blob for the TEXT data_encrypted column (server#71); keychain creds register + round-trip.
  • 2026-06-05 β€” noetl/worker v5.11.2 β€” resolves keychain alias under the v10 credential: key (worker#46).
  • 2026-06-05 β€” noetl/server v2.19.5 β€” v10 control-flow end-to-end (server#69, 6 commits): catalog INT4 + catalog_id alias, ToolSpec skip-null, orchestrator end-step-with-action + task_sequence flatten + intra-pass dedup, template Chainable- undefined, end-step trigger gate.
  • 2026-06-05 β€” noetl/worker v5.11.1 β€” preserve array tool_config for task_sequence (worker#44).
  • 2026-06-05 β€” noetl/tools v2.18.1 β€” task_sequence parse_tasks accepts worker-envelope shape.
  • 2026-06-04 β€” noetl/server v2.19.4 β€” orchestrator template context: step data at top level + call.done capture.
  • 2026-06-04 β€” noetl/tools v2.18.0 β€” TaskSequenceTool.
  • 2026-06-04 β€” noetl/tools v2.17.1 β€” PythonTool result- global capture.
  • 2026-06-04 β€” noetl/server v2.19.3 β€” three fixes shipped together: pipeline flat shape decode (#61), failure termination (#63), workbook resolution (#65).
  • 2026-06-04 β€” noetl/server v2.19.2 β€” v10 workload + input alias (#59).
  • 2026-06-04 β€” noetl/server v2.19.1 β€” EE-5 lax decode for integer execution_id (#57).
  • 2026-06-04 β€” noetl/server v2.19.0 β€” Phase F R4-4b (ExecutionService refactor + cluster-wide list fan-out).
  • 2026-06-04 β€” noetl/server v2.13.0 β†’ v2.19.0 β€” Phase F R4 series (DbPoolMap N+1 pool layer through R4-5 kind validation).
  • 2026-06-04 β€” noetl/gateway v3.2.0 β€” Phase F R3b-2 shard-info twin endpoint.

Conventions

How agents (Claude / Codex / Cursor) operate across this ecosystem β€” pointers into the rule files in agents/rules/:

  • Agent coordination β€” Claude ⇄ Codex lane ownership + shared-surface deconfliction (repos/server, pointer commits, prod image, kind) + append-only session board.
  • Issue tracking β€” when + how to open an ai-task issue (rule).
  • Continuous Integration β€” what the ai-meta CI gate checks (submodule pointers + docs), how to read a red pointer check and fix a bad bump, and the per-repo PR-gate status table. This repo had zero workflows until 2026-10-02, which is how #370 merged five pointer bumps unchecked.
  • Specs Index β€” the browsable map of general + cross-repo specs. Pointer list only: file = truth, issue = tracking, wiki = reading room, so a spec's acceptance criteria live in its file and are never copied here.
  • Wiki convention β€” ai-meta wiki vs per-repo wikis (rule). Rule 0a enumerates the four wiki pages that drift together; spot-check Home against the open-issue list at session start.
  • Handoffs β€” file-based cross-agent dispatch (rule).
  • Deployment validation β€” kind-first before GKE (rule).
  • Execution model β€” the foundational shape (rule).
  • Data access boundary β€” NoETL data via server API only; workers never touch noetl.* direct (rule).
  • Observability β€” metrics + traceability + snowflake IDs (rule).

How to use this dashboard

  • Just landed in this codebase? Read Repo Map, then Execution Model, then the umbrella for whatever you're working on.
  • Picking up an in-flight task? Find the matching umbrella page above; it has the full state of the work + the next concrete step.
  • Need to file new work? Follow the issue tracking convention β€” open the ai-task issue on noetl/ai-meta, then add the umbrella to the table above and create the corresponding wiki page.
  • Maintenance pass? Refresh this Home + the Sessions Log
    • the Releases page + the matching Umbrella-*.md page when you bump a submodule pointer. All four pages drift together β€” see Rule 0a's checklist.
⚠️ **GitHub.com Fallback** ⚠️