Skip to content

feat(cell): uptake cell-harness 0.3.0, agent-protocol 0.2.0, celld v0.3.0 - #156

Open
coccyx wants to merge 10 commits into
refactor/cell-infra-fleet-modulefrom
feat/uptake-cell-harness-0.3.0
Open

feat(cell): uptake cell-harness 0.3.0, agent-protocol 0.2.0, celld v0.3.0#156
coccyx wants to merge 10 commits into
refactor/cell-infra-fleet-modulefrom
feat/uptake-cell-harness-0.3.0

Conversation

@coccyx

@coccyx coccyx commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Takes into APM the framework changes that landed while Kidder was being built: the two npm packages, the celld 0.3.0 runtime, and the infra + docs changes needed to consume them.

Stacked on #155 (refactor/cell-infra-fleet-module) — the diff here shows only the uptake chunk. Merge #155 first.

Commits

  • 5dea567chore(deps): cell-harness 0.3.0, agent-protocol 0.2.0
  • cd73be7feat(cell/infra): celld v0.3.0, pinned module ref, optional secrets
  • 839ec0edocs(cell): DEPLOY/README for celld 0.3.0 + harness 0.3.0 vars
  • a session log commit

1. Deps — every change is additive, so no APM source edits

@criblio/agent-protocol  ^0.1.1 → ^0.2.0
@criblio/cell-harness    ^0.1.1 → ^0.3.0

Verified rather than assumed:

Framework change Why no APM edit
Env extends CellEnv CellEnv is a name-identical superset of what APM declared — the node's existing /etc/celld/env still binds. No SSM/Terraform change.
createTools(env, sql?) Optional param; APM's payload is API-backed and ignores sql.
imageCount? on tool frames Optional, and the UI imports wire types from @criblio/agent-protocol rather than mirroring them, so the type widens for free.
agent_images table CREATE TABLE IF NOT EXISTS in the session DO constructor ⇒ existing DOs gain it on rehydration. No migration.
DO class names CoordinatorDO / InvestigationDO unchanged ⇒ durable state stays addressable across the deploy.

The headline behavior change is the bounded watchdog: MAX_WATCHDOG_ATTEMPTS = 3, counted in durable storage under watchdogAttempts, written before the turn (the isolate dying is the event being counted). Previously unbounded — a turn whose tool overran celld's handler budget killed the process and got retried forever, restarting the node every ~240s.

2. Infra

  • celld v0.2.0v0.3.0.
  • CELLD_DURABILITY pinned to fleet. v0.3.0 added a replicated write-behind log and moved the default write-ack posture bucketfleet. That reads like a regression on a single-node fleet but isn't: with no peers celld behaves exactly like sync-to-bucket (bucket-proven acks) and starts using peers the moment one joins. Pinned rather than left to the default so a future celld default change can't move this node's durability without showing up in a plan.
  • CELLD_HANDLER_BUDGET_S pinned at the default, 300. Exceeding it kills the celld process, not just the isolate, so every session on the node dies — this is the budget the bounded watchdog stops retrying against. Pinned for visibility.
  • Module source is a pinned git ref, not a sibling checkout. The app stopped consuming the framework from file:../ when it moved to GitHub Packages, so nothing guarantees a checkout sits beside this one and terraform init on a fresh machine has to resolve without one. The repo is public, so https needs no credentials.
  • required_secret_keys = [UI_BEARER, TICKET_SECRET, LLM_API_KEY] — only these three abort the boot. WEBHOOK_BEARER only guards POST /alerts/fire, and the coordinator pulls firing alerts from $vt_results now, so autonomous investigation survives its absence; CRIBL_CLIENT_ID/SECRET are checked at use. Skipped keys land in /etc/celld/missing-secrets.
  • Staging config moved from -var flags into variables.tf defaults. All of it renders into user_data under user_data_replace_on_change = true, so an apply that forgot a flag would replace the node into stub mode (empty llm_base_url ⇒ no agent loop) with no error. bucket_name is the only flag now.
  • eslint.config.js ignores **/.terraform/ — the pinned git module means terraform init drops a full framework clone there and lint failed on vendored source. It's gitignored, but eslint doesn't read .gitignore.

Test plan

Local (all green on the tip):

npm run lint      # 0 errors
npm test          # 32 files, 440 tests passed
npx tsc --noEmit  # rc=0
cd cell && npx tsc --noEmit  # rc=0

Validate on staging — already done for the bundle

The new bundle is deployed and verified on the live node; sequenced deliberately one variable at a time so a bad bundle would still have a working node to roll back on.

  • celld deploy → version 9d5358e345bf0c41 (was 7bd185e1a91dd9d7), with deploy/current.json confirmed pointing at it before the restart. celld version IDs are content hashes and current.json is a separate pointer, so the bucket can hold a newer bundle than the pointer selects — that exact trap left the node on old code all of the previous day.
  • Bundle diff proves the new harness is in the artifact: watchdogAttempts present in the new index.js, absent from the old.
  • Restarted via SSM (graceful SIGTERM). No state lost: 30 investigations before → 30 after, 0 lost, 0 new, 0 title/status drift; all DOs restored fresh=false; journalctl -p warningNo entries; Started celld = 1 (no crash-loop).
  • Real end-to-end investigation on the new bundle (inv-ed5faf29-…): 69 frames, 19 real tool calls against live staging, found 393 error oteldemo.PaymentService/Charge spans at sub-ms latency.

Anyone can re-check right now:

curl -s https://54-71-34-177.sslip.io/healthz     # {"ok":true,"disabled":false}

Known limitations

  • The terraform apply has NOT been run. The celld 0.2.0 → 0.3.0 binary upgrade replaces the instance, so it needs a human at the confirmation prompt. Plan verified, blast radius from the plan JSON: module.fleet.aws_instance.cell (replace on user_data) + module.fleet.aws_eip_association.cell (replace on instance_id), 10 no-ops — 2 to add, 0 to change, 2 to destroy. The EIP is a separate resource so cell_url stays https://54-71-34-177.sslip.io and durable state survives in the bucket. Pre-flight done: user_data rendered through templatefile and bash -n'd, and the celld v0.3.0 aarch64 asset returns 200 (a 404 would brick the replacement node). The live node therefore still runs celld 0.2.0 with neither new env var; the bundle is already in place, so that apply is the only remaining step.
  • The verification investigation ended failed on "hit the 12-turn cap without concluding". Not a regression — the identical string is in the old bundle, and autonomous mode is hard-capped at MAX_TURNS = 12 regardless of TURN_BUDGET (sessionDO.js:832: interactive ? row.turn_budget : MAX_TURNS). Worth revisiting on its own.
  • Downgrading off celld v0.3.0 is one-way-ish: do not start a v0.2.x binary on a node that has run v0.3.0 unless its shutdown log shows node-log close: sealed epoch. Documented in variables.tf and cell/infra/README.md.

Session log: docs/sessions/2026-08-22-uptake-cell-harness-0.3.0.md

🤖 Generated with Claude Code

coccyx and others added 10 commits August 20, 2026 21:15
* docs: cell-harness extraction + Pi adoption design (spike: pi-agent-core turn-stepping PASS)

Design for splitting the generic celld agent harness out of cell/
into framework packages (@cribl/agent-protocol, cell-harness,
cell-workspace) behind a CellPayload seam, so a second app can run
a Pi coding agent in a celld worker without copying the APM cell.

Spike (spike-pi-agent-core.mjs, 5/5 PASS on 0.84.2): pi-agent-core
supports the DO alarm-per-turn model natively — shouldStopAfterTurn
stops after one LLM call with history ending on toolResult, messages
JSON round-trip, and a fresh Agent rehydrated from that JSON
continue()s to the answer. Root entry pulls no node builtins.

pi-coding-agent's tools are NOT worker-bundlable today (static node
+ TUI imports despite the Operations seam; no tools subpath export)
— recommendation is worker-native tools over the @cloudflare/computer
vfs + just-bash, borrowing its pure edit-diff module.

Also proposes publishing the framework as a proper npm module to
retire the .framework-sha pin (registry choice open).

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

* docs: record second-round decisions (GitHub Packages/@criblio, interactive-first, git write-back)

Registry: GitHub Packages under criblio — scope must match the org,
so packages publish as @criblio/*; @cribl/app-utils migration (rename
vs npm alias) deferred to the publishing PR. Coding app v1 is
interactive-first. Write-back is git: GitHub Data API backend first
(tarball checkout carries no .git state), isomorphic-git — including
a just-bash 'git' command wrapping it — as the upgrade path behind
the same tool surface.

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

* docs: run_search/run_metrics_query are framework-level — move their defs to app-utils in step 4

Answering the review question on PR #148: the executors already live
in app-utils/agent-tools (createRunSearchTool /
createRunMetricsQueryTool); APM only wires them and adds
render_trace. Step 4 also moves the shared tool definitions
(agentToolDefs schemas) into app-utils so other payloads can offer
the Cribl search tools without importing APM.

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

* docs: design status — steps 1-5a built as the PR stack

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… pi-agent-core runner, rebuild on framework packages (#149 #150 #151 #152)

Combined squash of the extraction stack (PRs #149-#152; #149/#150 were accidentally squash-merged into their stack bases rather than master, so this lands the full content — see the individual PRs for per-step detail and validation):

- #149 CellPayload seam: DOs reach APM behavior through the payload interface (cell/src/payloads/apm.ts)
- #150 wire protocol from @criblio/agent-protocol (both former mirrors deleted; LoopEvent parity type-test)
- #151 pi-agent-core turn runner (shouldStopAfterTurn alarm-per-turn; scripted-stream tests)
- #152 cell rebuilt on @criblio/cell-harness + cell-workspace; cell/src = APM payload + 16-line entry; DO class names/SQL/bindings frozen v1; esbuild bundle smoke verified

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158jez4WfWj6ttmaxh3HEbT
…sha (#153)

* feat: consume the framework from GitHub Packages — retire .framework-sha (extraction §6)

The framework is now published to GitHub Packages under @criblio
(framework PR #30): this switches every dependency to registry
versions and deletes the pin machinery.

- imports: @cribl/app-utils → @criblio/app-utils, @cribl/app-tooling
  → @criblio/app-tooling (54 files, mechanical)
- package.json: registry versions (^0.5.0 app-utils, ^0.2.0
  app-tooling, ^0.1.0 agent-protocol/cell-harness/cell-workspace)
  replacing file:../ links
- .npmrc: @criblio scope routing (no auth line — that lives in
  ~/.npmrc locally / is written by the release-build action in CI)
- ci.yml/release.yml: release-build action in registry mode
  (@7ef8eb4, registry-token: github.token); .framework-sha deleted
- new update-lockfile workflow: regenerates package-lock.json on a
  branch in CI for machines without a registry token
- CLAUDE.md: pin section replaced with GitHub Packages guidance
  (one-time ~/.npmrc auth, bump flow, framework-dev flow)

package-lock.json is regenerated by the update-lockfile workflow on
this branch (local machine has no registry token yet).

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

* chore: temporary push trigger to bootstrap the lockfile regen on this branch

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

* chore: regenerate package-lock.json (update-lockfile workflow)

* chore: bootstrap regen must drop the stale lockfile (file: resolutions were preserved)

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

* chore: regenerate package-lock.json (update-lockfile workflow)

* chore: restore update-lockfile to its utility form (bootstrap trigger removed)

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

* chore: preserve master's locked dep versions — resolve only @criblio fresh

The full lockfile regen floated every semver range; a newer
eslint-plugin-react-hooks brought new rules and 10 new lint errors,
which is version drift, not part of this change. Re-seed from
master's lockfile with the framework entries stripped so the CI
regen resolves exactly the 5 @criblio packages from the registry and
nothing else moves.

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

* chore: regenerate package-lock.json (update-lockfile workflow)

* chore: trigger CI (bot lockfile pushes don't start workflows)

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

* chore: bump framework packages to the built versions (0.5.1 / 0.1.1)

The dist+d.ts builds from framework PRs #32/#33 — source-shipped
packages broke Node consumers (type-stripping refuses node_modules).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ae/code/session_0158jez4WfWj6ttmaxh3HEbT

* chore: fix duplicate push key in bootstrap trigger (touch package.json to refire)

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

* chore: refire lockfile regen

* chore: widen bootstrap trigger paths so this push refires the regen

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

* chore: regenerate package-lock.json (update-lockfile workflow)

* chore: remove the bootstrap trigger — update-lockfile is dispatch-only from here

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Same-version deploys dedupe (deliberately no force), and 0.14.0 was
already installed; this ships the extraction-era pack (payload seam,
shared protocol, pi-agent-core runner, framework packages) to staging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158jez4WfWj6ttmaxh3HEbT
* fix(deps): react-router-dom ^7.18.2 — RSC-mode CSRF bypass (Dependabot #11)

High-severity advisory: RSC Mode CSRF bypass allows action execution
before the 400 response (react-router >=7.12.0 <7.18.2). This app
doesn't use RSC mode, so exposure is theoretical, but the bump is a
one-liner.

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

* chore: regenerate package-lock.json (update-lockfile workflow)

* chore: trigger CI (bot lockfile pushes don't start workflows)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Uptakes the framework's cell work since the extraction landed. Both
bumps are additive at the type level — the app and cell typecheck,
lint, and pass 440 tests unchanged — but two of the behaviors behind
them matter operationally:

- **Bounded watchdog retries** (cell-harness 0.3.0). The watchdog
  alarm re-ran a turn whose isolate died mid-run with no attempt
  counter, so a turn that kills its OWN isolate (a tool blowing
  celld's handler budget terminates the celld *process*) retried
  forever, restarting the node every 240s and taking every other
  session with it. Now capped at 3 attempts, counted in durable
  storage — the isolate dying is the event being counted, so the
  count has to outlive it. The new `watchdogAttempts` key defaults to
  absent, so existing DOs start with a clean slate.

- **Image input on interactive messages.** APM's UI doesn't attach
  images, so this is dormant here: `WireLoopEvent.userMessage` gained
  an optional `imageCount`, and the UI imports that type straight from
  @criblio/agent-protocol rather than mirroring it, so it widens with
  no edit. The new `agent_images` table is created with CREATE TABLE
  IF NOT EXISTS in the session DO's constructor, which runs on every
  rehydration — the 30 investigations already on the node gain it
  without a migration step.

`createTools` also gained an optional `sql` handle (PayloadSqlHandle)
for storage-backed payloads. APM's payload is API-backed and ignores
it; the param is optional, so apmPayload needs no change.

Not uptaken here: LLM_VISION. Declaring vision on a model that isn't
multi-modal is a hard API error on most providers, and the APM cell's
pinned model is text-only, so it stays off (the framework default).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three changes to the cell's fleet root, all riding one instance
replacement (user_data changes, and user_data_replace_on_change is on).
The plan is exactly `2 to add, 2 to destroy`: the instance and its EIP
association. Bucket, IAM, security group and the EIP itself are
no-ops — the sslip.io URL and every replicated cell SQLite survive, as
they did on the last two replacements.

**celld v0.2.0 → v0.3.0.** v0.3.0 adds a replicated write-behind log
and moves the default write-ack posture from `bucket` to `fleet`. On a
single-node fleet that is not the regression it looks like: with no
peers celld behaves exactly like sync-to-bucket (no record, no shipper,
bucket-proven acks) and starts using peers the moment one joins. It's
pinned via the new CELLD_DURABILITY rather than left implicit, so a
future celld default change can't move this node's durability without
appearing in a plan; `bucket` is the opt-out. CELLD_HANDLER_BUDGET_S is
pinned at celld's own default for visibility — blowing that budget
kills the celld PROCESS, so every session on the node dies, which is
the failure cell-harness 0.3.0's bounded watchdog stops retrying into.

Rolling back is not symmetric: a v0.2.x binary cannot read v0.3.0's
replicated log, so starting one on a node that has run v0.3.0 can lose
acknowledged writes unless the shutdown log shows `node-log close:
sealed epoch`. Documented in the README next to the version pin.

**Module source: sibling checkout → pinned git ref.** The path source
assumed a framework clone next to this repo, which was true when the
app consumed the framework via `file:../`. That ended when it moved to
GitHub Packages, so `terraform init` on a fresh machine or in CI had a
dependency nothing else in the repo still had. The framework repo is
public, so the https source needs no credentials.

**required_secret_keys: three of six.** Boot aborted on any missing
SSM parameter; now only the ones with no working degraded mode do.
UI_BEARER, because bearerOk() treats unset as closed and a node
without it serves 401 to the app — up and unusable is worse than not
up. TICKET_SECRET, which gates the WS transport. LLM_API_KEY, because
an investigator that can't reach a model has nothing to offer. The
rest degrade honestly and shouldn't cost the node: WEBHOOK_BEARER only
guards /alerts/fire, and the coordinator PULLS from $vt_results now
(verified: pollTriggers authenticates with the Cribl creds, not the
webhook bearer), while CRIBL_CLIENT_ID/SECRET are checked at use and
report themselves in the transcript. All six exist in SSM today, so
this changes nothing about the current node — it decides what happens
to the next one.

Also pins the agent-loop config as variable defaults instead of
runbook `-var` flags. All four values render into user_data, so an
apply that forgot one would replace the node into stub mode (empty
llm_base_url ⇒ no agent loop) with no error at all.

Verified: terraform fmt/validate clean; `init` resolves the pinned
module over https with no token; plan against live state shows the
2-add/2-destroy shape above and the module's required_secret_keys
subset validation passing (that check runs at PLAN time, not during
validate); the rendered user_data passes `bash -n` and carries
CELLD_VERSION=v0.3.0, CELLD_DURABILITY=fleet,
CELLD_HANDLER_BUDGET_S=300 and REQUIRED_KEYS; and the v0.3.0
aarch64 release asset resolves 200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Documents the operational surface the previous two commits changed,
and stops eslint walking Terraform's module cache.

- DEPLOY.md: step 2 is `terraform apply -var bucket_name=…` and
  nothing else now that the agent-loop config lives in variables.tf
  defaults; the celld-bump note points at `var.celld_version` and
  carries the v0.3.0 rules (roll-forward-only off 0.3.0). The CI/CD
  sketch needs a GitHub Packages auth line — the cell's deps are
  @criblio packages since .framework-sha retired — and downloads
  celld v0.3.0 to match the node's binary.
- cell/README.md: document LLM_VISION and TURN_BUDGET. Vision stays
  off deliberately: the pinned model is text-only, pi-ai silently
  drops images for an undeclared model, and most providers hard-fail
  image parts sent to a text-only one.
- eslint.config.js: ignore `**/.terraform/`. `cell/infra` now pins
  the celld-fleet module by git ref, so `terraform init` drops a full
  framework clone in there and lint failed on vendored source
  (react-refresh/only-export-components, two files). It's gitignored,
  but eslint doesn't read .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records what shipped, the deploy sequencing (bundle first on the old
node, instance replacement second), the verification evidence, and the
traps — content-hash version IDs vs the current.json pointer, the
one-way v0.3.0 downgrade, and celld 0.2.0 having no version endpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedreact-router-dom@​7.18.1 ⏵ 7.18.21001006594 -1100
Added@​criblio/​agent-protocol@​0.2.0N/AN/AN/AN/AN/A
Added@​criblio/​app-tooling@​0.2.0N/AN/AN/AN/AN/A
Added@​criblio/​app-utils@​0.5.1N/AN/AN/AN/AN/A
Added@​criblio/​cell-harness@​0.3.0N/AN/AN/AN/AN/A
Added@​criblio/​cell-workspace@​0.1.1N/AN/AN/AN/AN/A

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant