Skip to content

fix(sessions): classify spawn-child sessions correctly; extract shared classifier#79544

Merged
galiniliev merged 6 commits into
openclaw:mainfrom
efpiva:edpiva/acp-kind-spawn-child
May 13, 2026
Merged

fix(sessions): classify spawn-child sessions correctly; extract shared classifier#79544
galiniliev merged 6 commits into
openclaw:mainfrom
efpiva:edpiva/acp-kind-spawn-child

Conversation

@efpiva
Copy link
Copy Markdown
Contributor

@efpiva efpiva commented May 8, 2026

Summary

ACP spawn-child sessions (Telegram supergroup-spawned children, etc.) were misclassified as kind: "direct" in openclaw sessions listings. Add a new "spawn-child" kind, extract the duplicated session classifier into a shared helper, and make the new kind authoritative when entry.spawnedBy is set. Closes catalog #19.

Bug detail

ACP sessions with spawnedBy: "agent:main:telegram:group:-1003967207344:topic:1" and a populated deliveryContext were reported as kind: "direct" in openclaw sessions --json. Most ACP sessions in deployed listings showed direct even though they had been spawned from a group with a bound thread context. Operators relying on the kind field for dashboards / triage filters were missing spawn-child sessions entirely.

How to reproduce

Live repro before fix:

```bash
docker compose exec codeclaw-openclaw openclaw sessions --all-agents --json
| jq '.sessions[] | select(.key | contains("acp:")) | {key, kind, deliveryContext}'

Before: ACP sessions with spawnedBy still show "kind": "direct"

After: sessions with spawnedBy show "kind": "spawn-child"

```

Unit-level:

```bash
git checkout 8e48d27 # red-test cherry-pick
pnpm test src/commands/sessions.kind-classification.test.ts

Before fix: 2 RED + 2 GREEN controls

After fix commits a48d6d5 + d89ec0c: all 4 GREEN

```

Root cause

classifySessionKey was implemented twice — at src/commands/sessions.ts:136-152 and duplicated at src/commands/status.summary.runtime.ts:129-145. Both classified by session-key shape only, not by entry metadata. ACP-style keys carry no embedded channel/thread indicator, so spawn-child ACP entries fell through to "direct". The spawnedBy field on the entry was never consulted.

Fix approach

  1. Extract the duplicated classifier into a new shared helper at src/sessions/classify-session-kind.ts (classifySessionKind). Existing logic preserved.
  2. Add an early entry?.spawnedBy truthy check that returns "spawn-child" BEFORE the key-shape branch — so spawn-child ACP sessions take precedence over the (now-correct but no-longer-default) shape-based fallback.
  3. Add "spawn-child" to the SessionKind union and to SessionRow["kind"] in src/commands/sessions.ts.
  4. Replace the inline classifier in src/commands/status.summary.runtime.ts with the shared import. The existing public classifySessionKey API surface is preserved via an alias on statusSummaryRuntime, so the existing test in status.summary.runtime.test.ts and the consumer in status.summary.ts need no changes.
  5. Widen the KIND_PAD constant in sessions.ts from 6 to 11 to fit the new longest label and keep the table column aligned.

Tests

  • Red test (now GREEN): src/commands/sessions.kind-classification.test.ts — 4 cases (2 RED → GREEN, 2 GREEN controls stay GREEN).
  • Adjacent suite: src/commands/ — 196 files / 1502 tests pass.

Catalog reference

Catalog finding: #19. Investigation lives in branch edpiva/acp-native-session-investigation (not yet upstream) at docs/plans/2026-05-08-acp-findings-catalog.md.

Verification commands

```bash
pnpm test src/commands/sessions.kind-classification.test.ts # 4/4 GREEN
pnpm test src/commands/ # 1502/0
```

Notes for reviewers

  • A separate classifier exists at src/gateway/session-utils.ts:858 for the gateway RPC surface (GatewaySessionRow["kind"] = "direct" | "group" | "global" | "unknown" — narrower contract). It is intentionally NOT touched by this PR; if the gateway listing surface should also expose "spawn-child", that's a separate follow-up.
  • SessionKind is intentionally a different name than the existing SessionKind in src/agents/tools/sessions-helpers.ts — different domains, different unions, no shared imports. They co-exist safely.
  • formatKindCell has a fallthrough that handles "spawn-child" by displaying it muted. The explicit "spawn-child" branch was removed because it was identical to the fallthrough.

Live behavior repro (deployed container)

Container: codeclaw-openclaw:clean running OpenClaw 2026.5.6 (e9d4a0a)
PII note: Telegram chat/group/topic IDs redacted to <chat-id> / <topic-id>.

Underlying store DOES carry spawnedBy, but openclaw sessions --json projects it away → kind defaults to "direct"

The disk-side store at ~/.openclaw/agents/copilot/sessions/sessions.json records the spawning context for every spawn-child entry:

agent:copilot:acp:8fb42bab-…   sessionFile=…/e421061f-…   spawnedBy=agent:main:telegram:default:direct:<chat-id>
agent:copilot:acp:f5ab8dba-…   sessionFile=…/18a09361-…   spawnedBy=agent:main:telegram:group:<chat-id>:topic:<topic-id>
agent:copilot:acp:d6fb7506-…   sessionFile=…/1978341a-…   spawnedBy=agent:main:telegram:group:<chat-id>:topic:<topic-id>
agent:copilot:acp:86b7b5af-…   sessionFile=…/7de23a0a-…   spawnedBy=agent:main:telegram:group:<chat-id>:topic:<topic-id>

But the same sessions in openclaw sessions --all-agents --json report spawnedBy: null and kind: "direct" (or "group" for some) — the underlying signal isn't reaching the row classifier:

{"key":"agent:copilot:acp:86b7b5af-…","kind":"direct","spawnedBy":null,"deliveryContext":null,}
{"key":"agent:copilot:acp:f5ab8dba-…","kind":"direct","spawnedBy":null,"deliveryContext":null,}
{"key":"agent:copilot:acp:d6fb7506-…","kind":"group","spawnedBy":null,"deliveryContext":null,}

After this PR, classifySessionKind checks entry?.spawnedBy truthy BEFORE the key-shape branch, so spawn-child rows resolve to kind: "spawn-child" and operators can filter dashboards correctly.

(The follow-up gap noted by the reviewer — src/gateway/session-utils.ts still uses the narrower GatewaySessionRow["kind"] enum without "spawn-child" — is intentionally out of scope for this PR; tracked for a follow-up.)


Updates

2026-05-08 (follow-up commit 14c84f2bf8): Fixed post-push CI regression.

src/commands/status.types.ts had SessionStatus.kind typed as a hardcoded literal union "cron" | "direct" | "group" | "global" | "unknown" — the PR's new "spawn-child" member was not included, causing TS2322 across all type-check and build shards (check-prod-types, check-test-types, check-lint, build-artifacts, check-strict-smoke, check-additional-extension-*, build-smoke). Fix: import and use the canonical SessionKind type from src/sessions/classify-session-kind.ts so the union stays in sync automatically. The security-dependency-audit failure (fast-uri CVE GHSA-v39h-62p7-jpjc) is pre-existing on main (green on commit ac5acaeb) and unrelated to this PR.


Real behavior proof

  • Behavior or issue addressed: ACP spawn-child sessions with persisted spawnedBy metadata classify as kind: "spawn-child" instead of falling through to "direct" in openclaw sessions and status output.
  • Real environment tested: Azure Crabbox Linux lease brisk-barnacle / cbx_5c3278f7e9f2, fresh checkout of PR head 996e78598f958efb2e86ab381eda6c08eefdd8cc, Node 22.22.2, seeded OpenClaw session store with ACP spawn-child and direct-control rows.
  • Exact steps or command run after this patch:
pnpm test src/commands/sessions.kind-classification.test.ts
pnpm build
pnpm openclaw sessions --all-agents --json  # against a seeded ACP spawn-child session store
pnpm openclaw status --json                 # against the same seeded store
node scripts/run-oxlint.mjs src/sessions/classify-session-kind.ts src/commands/sessions.ts src/commands/status.summary.runtime.ts src/commands/status.types.ts src/commands/sessions.kind-classification.test.ts
  • Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output): Maintainer-collected terminal output from Azure Crabbox:
AFTER_FIX_ACP_ROW={"key":"agent:copilot:acp:<redacted>","kind":"spawn-child"}
CONTROL_DIRECT_ROW={"key":"agent:copilot:acp:<redacted-direct>","kind":"direct"}
STATUS_AFTER_FIX_ACP_ROW={"key":"agent:copilot:acp:<redacted>","kind":"spawn-child"}
Test Files 1 passed
Tests 4 passed
pnpm build passed
Found 0 warnings and 0 errors.
  • Observed result after fix: openclaw sessions --all-agents --json reported the seeded ACP child row as kind: "spawn-child"; the ACP direct-control row stayed kind: "direct"; openclaw status --json also reported the ACP child row as kind: "spawn-child".
  • What was not tested: Live Telegram/Copilot ACP auth was not exercised in this maintainer proof; the after-fix behavior proof used a seeded session store that mirrors the deployed store shape from the before-fix repro.

@openclaw-barnacle openclaw-barnacle Bot added commands Command implementations size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 8, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 8, 2026

Codex review: needs maintainer review before merge.

Summary
The PR extracts session-kind classification into a shared helper, adds a spawn-child kind based on SessionEntry.spawnedBy, wires openclaw sessions and status output to it, updates tests, and adds a changelog entry.

Reproducibility: yes. source-reproducible with high confidence: current main passes session entries into duplicated classifiers that ignore spawnedBy, so opaque ACP child keys fall through to direct. The PR also includes before-fix deployed output and after-fix seeded CLI/status proof.

Real behavior proof
Sufficient (terminal): After-fix Crabbox terminal output shows the seeded ACP child row classified as spawn-child in both sessions and status output, with a direct-control row still direct.

Next step before merge
No ClawSweeper repair job is needed because the PR already contains the focused code change, regression coverage, changelog entry, and sufficient real behavior proof.

Security
Cleared: The diff only changes TypeScript session classification, CLI/status types, focused tests, and changelog text, with no dependency, CI, secret, install, or release-script surface touched.

Review details

Best possible solution:

Merge the shared classifier after normal maintainer and CI checks; keep gateway session-row kind parity as a separate follow-up if maintainers want that API widened.

Do we have a high-confidence way to reproduce the issue?

Yes, source-reproducible with high confidence: current main passes session entries into duplicated classifiers that ignore spawnedBy, so opaque ACP child keys fall through to direct. The PR also includes before-fix deployed output and after-fix seeded CLI/status proof.

Is this the best way to solve the issue?

Yes. Checking existing spawnedBy metadata before key-shape fallback and sharing the duplicated classifier is the narrow maintainable fix for the CLI/status surfaces in scope.

What I checked:

  • Current main CLI classifier ignores spawn lineage: openclaw sessions still classifies by sentinel keys, cron key shape, chatType, and :group:/:channel: substrings, then falls through to direct; the current classifier signature does not include or read spawnedBy. (src/commands/sessions.ts:175, 8046b5e4621b)
  • Current main status classifier has the same gap: The status summary runtime duplicates the shape-only classifier and returns direct when an opaque ACP key has no group/channel key marker, even if the session entry carries spawn metadata. The status summary builder passes the entry into this classifier. (src/commands/status.summary.runtime.ts:128, 8046b5e4621b)
  • Spawn lineage is existing session metadata: SessionEntry already defines spawnedBy?: string as the parent session key for spawned sessions, so the PR uses an existing store signal rather than inventing a new persisted field. (src/config/sessions/types.ts:199, 8046b5e4621b)
  • PR head implements the focused classifier change: The supplied PR diff adds src/sessions/classify-session-kind.ts, defines SessionKind with spawn-child, and checks entry?.spawnedBy before group/direct fallback while preserving sentinel and cron precedence. (src/sessions/classify-session-kind.ts:3, 936aebea6be8)
  • PR head wires both user-facing surfaces: The supplied PR diff replaces the inline CLI/status classifiers with the shared helper, widens SessionStatus.kind through the canonical SessionKind type, and pads the table kind column for the longer label. (src/commands/status.types.ts:1, 936aebea6be8)
  • After-fix real behavior proof is now supplied: The PR body and maintainer comment include Azure Crabbox terminal output from PR head 996e78598f958efb2e86ab381eda6c08eefdd8cc showing a seeded ACP child row as kind: "spawn-child" in both openclaw sessions --all-agents --json and openclaw status --json, with a direct-control ACP row staying direct; the same proof reports the focused test, build, and oxlint checks passing. (996e78598f95)

Likely related people:

  • efpiva: Prior merged current-main work on src/commands/sessions.ts added ACP runtime display behavior adjacent to this classifier, beyond authoring this PR. (role: recent adjacent sessions contributor; confidence: high; commits: 207fb9951d67; files: src/commands/sessions.ts)
  • vincentkoc: Recent merged commits changed the central sessions table and status CLI session surfaces that this PR updates. (role: recent sessions/status contributor; confidence: high; commits: c874c0863ada, 46c99cff0b2f; files: src/commands/sessions.ts, src/commands/status.summary.runtime.ts, src/commands/status.types.ts)
  • mbelinky: The spawnedBy signal this PR classifies is tied to prior ACP spawned child session history persistence work. (role: spawned-session lineage contributor; confidence: medium; commits: 404b1527e6bd; files: src/config/sessions/types.ts, src/agents/acp-spawn.ts)
  • steipete: History shows earlier session-key classifier reuse/refactor work and recent command/session maintenance on the same files. (role: session classifier and command-area contributor; confidence: medium; commits: 94eb50658db5, 3efd224ec63d; files: src/commands/sessions.ts, src/commands/status.summary.runtime.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 8046b5e4621b.

@efpiva
Copy link
Copy Markdown
Contributor Author

efpiva commented May 8, 2026

Live behavior repro (deployed container)

Container: codeclaw-openclaw:clean running OpenClaw 2026.5.6 (e9d4a0a)
PII note: Telegram chat/group/topic IDs redacted to <chat-id> / <topic-id>.

Underlying store DOES carry spawnedBy, but openclaw sessions --json projects it away → kind defaults to "direct"

The disk-side store at ~/.openclaw/agents/copilot/sessions/sessions.json records the spawning context for every spawn-child entry:

agent:copilot:acp:8fb42bab-…   sessionFile=…/e421061f-…   spawnedBy=agent:main:telegram:default:direct:<chat-id>
agent:copilot:acp:f5ab8dba-…   sessionFile=…/18a09361-…   spawnedBy=agent:main:telegram:group:<chat-id>:topic:<topic-id>
agent:copilot:acp:d6fb7506-…   sessionFile=…/1978341a-…   spawnedBy=agent:main:telegram:group:<chat-id>:topic:<topic-id>
agent:copilot:acp:86b7b5af-…   sessionFile=…/7de23a0a-…   spawnedBy=agent:main:telegram:group:<chat-id>:topic:<topic-id>

But the same sessions in openclaw sessions --all-agents --json report spawnedBy: null and kind: "direct" (or "group" for some) — the underlying signal isn't reaching the row classifier:

{"key":"agent:copilot:acp:86b7b5af-…","kind":"direct","spawnedBy":null,"deliveryContext":null,}
{"key":"agent:copilot:acp:f5ab8dba-…","kind":"direct","spawnedBy":null,"deliveryContext":null,}
{"key":"agent:copilot:acp:d6fb7506-…","kind":"group","spawnedBy":null,"deliveryContext":null,}

After this PR, classifySessionKind checks entry?.spawnedBy truthy BEFORE the key-shape branch, so spawn-child rows resolve to kind: "spawn-child" and operators can filter dashboards correctly.

(The follow-up gap noted by the reviewer — src/gateway/session-utils.ts still uses the narrower GatewaySessionRow["kind"] enum without "spawn-child" — is intentionally out of scope for this PR; tracked for a follow-up.)

efpiva added a commit to efpiva/openclaw that referenced this pull request May 9, 2026
@galiniliev
Copy link
Copy Markdown
Contributor

Maintainer-collected after-fix behavior proof for #79544.

Provider: Azure Crabbox
Slug / lease: brisk-barnacle / cbx_5c3278f7e9f2
Remote workdir: /work/crabbox/cbx_5c3278f7e9f2/fresh-pr-openclaw-openclaw-79544
PR head tested: 996e78598f958efb2e86ab381eda6c08eefdd8cc
Local evidence file: .crabbox/evidence/pr79544-after-fix-proof-20260513T223212Z.txt

Exact command surface exercised after this patch:

pnpm test src/commands/sessions.kind-classification.test.ts
pnpm build
pnpm openclaw sessions --all-agents --json  # against a seeded ACP spawn-child session store
pnpm openclaw status --json                 # against the same seeded store
node scripts/run-oxlint.mjs src/sessions/classify-session-kind.ts src/commands/sessions.ts src/commands/status.summary.runtime.ts src/commands/status.types.ts src/commands/sessions.kind-classification.test.ts

After-fix proof lines:

AFTER_FIX_ACP_ROW={"key":"agent:copilot:acp:<redacted>","kind":"spawn-child"}
CONTROL_DIRECT_ROW={"key":"agent:copilot:acp:<redacted-direct>","kind":"direct"}
STATUS_AFTER_FIX_ACP_ROW={"key":"agent:copilot:acp:<redacted>","kind":"spawn-child"}

Verification results:

Test Files 1 passed
Tests 4 passed
pnpm build passed
Found 0 warnings and 0 errors.

Proof scope: this is after-fix CLI/status behavior from a seeded session store on the PR branch. It is not live Telegram/Copilot ACP auth proof.

efpiva added 5 commits May 13, 2026 23:20
… type

The PR added "spawn-child" to SessionKind in classify-session-kind.ts but
status.types.ts still had a hardcoded literal union that omitted it, causing
TS2322 across all typecheck + build CI shards.
@galiniliev galiniliev force-pushed the edpiva/acp-kind-spawn-child branch from 996e785 to 6b5294c Compare May 13, 2026 23:24
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 13, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 13, 2026
@galiniliev
Copy link
Copy Markdown
Contributor

Landing verification for PR #79544 at head 936aebea6be88eec5de501d52206b18a45bdcd26.

Behavior addressed: ACP spawn-child sessions with persisted spawnedBy metadata classify as kind: "spawn-child" instead of falling through to "direct" in openclaw sessions and status output.

Real environment tested: Azure Crabbox Linux lease brisk-barnacle / cbx_5c3278f7e9f2, fresh checkout of PR head 996e78598f958efb2e86ab381eda6c08eefdd8cc, Node 22.22.2, seeded OpenClaw session store with ACP spawn-child and direct-control rows.

Exact steps or command run after this patch:

pnpm test src/commands/sessions.kind-classification.test.ts
pnpm build
pnpm openclaw sessions --all-agents --json
pnpm openclaw status --json
node scripts/run-oxlint.mjs src/sessions/classify-session-kind.ts src/commands/sessions.ts src/commands/status.summary.runtime.ts src/commands/status.types.ts src/commands/sessions.kind-classification.test.ts

Additional local verification after rebase/test-fix on head 936aebea6be88eec5de501d52206b18a45bdcd26:

pnpm test src/commands/sessions.kind-classification.test.ts src/commands/status.summary.runtime.test.ts
OPENCLAW_VITEST_INCLUDE_FILE=<generated agentic-commands-status-tools file> OPENCLAW_VITEST_SHARD_NAME=agentic-commands-status-tools OPENCLAW_TEST_PROJECTS_PARALLEL=2 OPENCLAW_VITEST_MAX_WORKERS=2 NODE_OPTIONS=--max-old-space-size=8192 pnpm exec node scripts/test-projects.mjs test/vitest/vitest.commands.config.ts
node scripts/run-oxlint.mjs src/sessions/classify-session-kind.ts src/commands/sessions.ts src/commands/status.summary.runtime.ts src/commands/status.types.ts src/commands/sessions.kind-classification.test.ts src/commands/sessions.test.ts

Evidence after fix:

AFTER_FIX_ACP_ROW={"key":"agent:copilot:acp:<redacted>","kind":"spawn-child"}
CONTROL_DIRECT_ROW={"key":"agent:copilot:acp:<redacted-direct>","kind":"direct"}
STATUS_AFTER_FIX_ACP_ROW={"key":"agent:copilot:acp:<redacted>","kind":"spawn-child"}
Test Files 1 passed
Tests 4 passed
pnpm build passed
Found 0 warnings and 0 errors.

Observed result after fix: openclaw sessions --all-agents --json reported the seeded ACP child row as kind: "spawn-child"; the ACP direct-control row stayed kind: "direct"; openclaw status --json also reported the ACP child row as kind: "spawn-child".

CI/Testbox run IDs checked before landing:

  • CI run 25832353554: check, check-lint, check-prod-types, check-test-types, build-artifacts, build-smoke, checks-node-agentic-commands-status-tools, and aggregate node/check shards passed.
  • Real behavior proof run 25832651473 passed after the proof body update.
  • OpenGrep PR Diff run 25832353599 passed.
  • CodeQL High run 25832353535 passed.
  • CodeQL Critical Quality run 25832353567 passed or intentionally skipped selected shards.

What was not tested: live Telegram/Copilot ACP auth was not exercised in this maintainer proof; the after-fix behavior proof used a seeded session store that mirrors the deployed store shape from the before-fix repro.

@galiniliev galiniliev merged commit 9431d18 into openclaw:main May 13, 2026
114 checks passed
@galiniliev galiniliev self-assigned this May 14, 2026
@galiniliev galiniliev self-requested a review May 14, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands Command implementations proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants