anthropics / anthropics/claude-agent-sdk-typescript

Slash command XML wrapper persists in session transcript and leaks through resumed history

未關閉
#314 1 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視
bug enhancement
主要語言
Shell
星號
1.8k
分支
226
PR 合併指標
30 天內沒有已合併 PR

描述

## Summary

When a user invokes a slash command (e.g. `/context`) via `query()`, the SDK wraps the input in a fixed `` / `` / `` XML envelope before persisting it to the session JSONL transcript. On session resume — either via `query({ resume })` or via `getSessionMessages(sessionId)` — that wrapper is returned verbatim as the user-message `text` content. SDK consumers (chat UIs, IDE integrations, web wrappers) cannot distinguish this internal serialisation from real user text, so it ends up either rendered to the user as raw XML or silently re-sent into the model as part of the next prompt.

## Environment

- `@anthropic-ai/claude-agent-sdk`: observed on `0.2.121` (latest at time of writing: `0.2.128`)
- Node.js: 22.x
- Consumer: an Express + WebSocket UI built on `query()` + `getSessionMessages()`

## Reproduction

1. Start a session with `query({ prompt, options: { ... } })`.
2. Send `/context` (or any built-in slash command) as the user prompt.
3. Let the SDK respond, then end the stream.
4. From a separate process / fresh import, call `getSessionMessages(sessionId, { dir })` for the same session — or open a new `query({ resume: sessionId })` and inspect the assistant's incoming context.
5. Inspect the user turn.

## Actual behaviour

The user message content arrives as a single `text` block whose `text` is the literal SDK wrapper:

```
/context
context

```

Concrete consequences for downstream consumers:

- A chat UI that renders message text directly displays raw XML tags after a page reload / session resume, where on first execution it had shown a clean `/context` invocation.
- If the consumer forks from or appends to this turn, the wrapper is fed back into the model on the next request. The model then sees a tag soup it cannot meaningfully act on.

## Expected behaviour

Any one of the following would be a substantial DX improvement, in rough order of preference:

1. **Preferred** — `getSessionMessages()` returns the original user input (`/context`) as the visible text, and exposes the slash-command metadata as a separate, structured field on the message (e.g. `slashCommand: { name: '/context', args: '' }`). Consumers that want the wrapper format keep it; consumers that just want to display history get clean text.
2. Persist both the original input and the wrapper in the JSONL transcript, so consumers can choose which one to surface.
3. At minimum, document the wrapper format in the README and export a public unwrap helper (e.g. `unwrapSlashCommandWrapper(text: string): { name: string; args: string } | null`). This keeps third-party consumers from each writing their own brittle regex against an undocumented internal serialisation.

## Why this matters

Every SDK consumer that exposes slash commands and supports session resume hits this. The official Claude Code CLI sidesteps the symptom because most built-in slash commands (`/context`, `/help`, `/clear`, …) are handled by the harness ephemerally and never enter the transcript at all — that escape hatch is not available to SDK consumers, who must use whatever shape the SDK persists.

## Current workaround (used in our codebase)

We added an `unwrapSlashCommandWrapper()` helper at our normalisation layer:

```ts
const SLASH_CMD_WRAPPER_RE =
/^\s*([^<]+)<\/command-name>\s*[^<]*<\/command-message>\s*([^<]*)<\/command-args>\s*/;

function unwrapSlashCommandWrapper(text: string): string {
const m = SLASH_CMD_WRAPPER_RE.exec(text);
if (!m) return text;
const name = m[1].trim();
const args = m[2].trim();
const trailing = text.slice(m[0].length);
const restored = args ? `${name} ${args}` : name;
return trailing ? `${restored}\n${trailing}` : restored;
}
```

This works but is fragile — it pins the consumer to the exact wrapper format the SDK happens to emit today. A first-class SDK API would be much more robust.

## Related

- #302 covers a different slash-command edge case (non-resumable `session_id` for unsupported headless commands). The two are independent — this one is about wrapper format leaking *through* a successfully resumed session.

貢獻指南

這個儲存庫沒有索引到貢獻指南

評估

這個 Issue 還沒有評估資料。

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。