docs: remove completed design and planning documents

This commit is contained in:
somew1nd committed 2026-09-02 00:12:33 -07:00
1 parent b8ecda9dd1
commit 99343dfe55
7 files changed
-3745

No files matched your search

-913
View File
@@ -1,913 +0,0 @@
# ARTEMIS Pro Context, History, and Summarization Redesign
Status: Proposal (2026-08-25) — partially superseded
> **2026-08-31**: `docs/plans/history-module-redesign.md` (unified v3) supersedes
> §6/§11/§12 of this document: Pro moves from per-turn prompt recompilation to an
> incremental append-only transcript with layered compaction (this doc's Phase 5
> promoted to the core architecture). §7 (data model), §8 (visually grounded
> neutral summaries), §9 (stable chunks), §10 (recall), §13 (same-turn envelopes)
> are absorbed and remain authoritative where referenced.
>
> **Final status (2026-09-01)**: the unified v3 plan is fully implemented
> (M0–M5, work in worktree `zealous-gates-4626b1`, uncommitted). Everything
> this document delegated forward has landed, including §3.3 (the
> `<short_term_memory>` relay was removed end to end in M5 — state field,
> operator extraction, prompt component, visibility manifest entries, and the
> operator.json instruction block; render-time tag scrubbing is kept for older
> recorded responses, per this section's own "safely ignoring" clause). See
> history-module-redesign.md §7 for the per-milestone implementation notes.
Scope: ARTEMIS Pro / LangGraph execution mode
Audience: maintainers of Operator, Validator, Summarizer, DataEngine, and model routing
## 1. Executive Summary
This proposal improves long-term history, visually grounded neutral summaries,
context compression, and history recall without weakening Pro mode's current
reasoning continuity. The goal is not to give Operator more raw history by
default. It is to increase information density while preserving the current
`10K-12K` input range and moving all additional image interpretation to a
non-blocking background path.
The design preserves mechanisms that already work:
- the previous Operator step's complete raw or native thinking;
- the exact previous action and Validator result;
- the current screenshot and UI element list;
- complete, auditable raw step records in DataEngine; and
- asynchronous Summarizer scheduling that never blocks the graph's critical path.
It adds the following capabilities:
1. Preserve the previous model message, thought signature, action call, and Validator result as a continuous conversation tail.
2. Keep the current neutral summary contract while allowing the Summarizer to use the decision screenshot and an optional post-action screenshot as context.
3. Bind thinking, actions, available images, validation results, and summaries with a stable `step_id`.
4. Treat the active milestone only as a temporary working-set label; use plan-independent history chunks for long-term compression.
5. Add a bounded, on-demand `recall_history` tool for complete steps, older screenshots, and UI text.
6. Introduce a fixed token budget, value-density history selection, and same-turn tool-output limits.
7. Separate stable system rules from dynamic plans and history to improve prefix caching and multi-turn continuity.
The steady-state Operator context becomes:
```text
Current screenshot/XML
+ complete previous-turn reasoning and action result
+ recent visually grounded step summaries
+ relevant history-chunk summaries
+ cold-history recall on demand
```
The steady-state target remains `10K-12K tokens`, with zero additional model
calls and zero summary waits on the critical path.
## 2. Goals and Constraints
### 2.1 First Priority: Preserve Speed
The engineering definition of preserving speed is:
- no new synchronous model calls in normal steps;
- Operator never waits for summaries, indexes, or history chunks;
- first-call input P50 and P95 remain close to the current baseline;
- background summaries do not compete at the same request priority as Operator;
- normal Operator requests still contain only one current screenshot;
- history improvements replace low-density content instead of expanding normal context; and
- any background failure immediately falls back to the current detailed history.
Absolute zero latency change cannot be guaranteed by adding synchronous input.
This design therefore uses a token-neutral, zero-wait critical path and validates
P50 and P90 through paired A/B tests.
### 2.2 History and Summary Goals
- Preserve the previous strategy, expected result, and unresolved questions.
- Ground older step summaries in available visual evidence instead of guessing from action text.
- Keep important history visible across plan rewrites, milestone renames, and goal rollback.
- Make every compressed record traceable to original steps, screenshots, and tool traces.
- Never discard failures, recovery work, side effects, user constraints, or critical values through ordinary compression.
- Allow Operator to recall older information during long tasks.
### 2.3 Use Model Capacity Effectively
"Maximize" means maximizing useful reasoning information, not filling a one-million-token window:
- the strongest model directly handles current visual state, continuous reasoning, and high-value history;
- available older screenshots help the Summarizer understand the UI, action target, and explicit records;
- long context acts as an exception and deep-recall buffer, not the normal working set;
- native multi-turn messages and thought signatures are preserved;
- stable system prefixes are reused whenever possible; and
- the model does not repeatedly reread duplicate plans, raw logs, or low-value click traces.
### 2.4 Non-Goals
- Do not delete or overwrite raw history in DataEngine.
- Do not send multiple historical screenshots to Operator by default.
- Do not treat plan milestones as permanent fact boundaries.
- Do not replace Validator's objective judgment with a summary.
- Do not require a new model API in the first phase.
- Do not enlarge every step merely to use a larger context window.
## 3. Current Baseline
### 3.1 Pro Graph Path
```text
Planner
-> Convergence
-> Perception
-> Operator
-> Execution Check
-> Validator
-> Summarizer
-> Convergence
```
The Summarizer node creates a background task and returns immediately. This
behavior must remain unchanged.
### 3.2 Current Operator History
`build_plan_and_history` currently:
- renders the latest completed step in detail;
- may include raw/native thinking, actions, tool calls, Validator results, and Failure Analyzer traces;
- renders older summaries when available while retaining exact actions;
- preserves steps under the active subgoal plus the latest three global steps;
- puts the full current plan in the Operator prompt; and
- rebuilds messages on every Operator turn.
The current system already has continuity. Removing complete reasoning from the
latest step would prevent the model from understanding the previous strategy,
expectation, and decision basis.
### 3.3 Remove Separate Short-Term Memory
Remove the existing `<short_term_memory>` extraction, state storage, and next-turn
injection path without introducing a replacement relay object. Cross-turn
continuity should come from the complete previous Operator message, exact action,
Validator result, current plan, long-term Notes, and auditable execution history.
### 3.4 Current Pro Summaries
The current Pro Summarizer receives only text:
- current plan and history;
- current-step thinking;
- action and execution result; and
- Failure Analyzer trace.
It does not load `pre_image_name` or `post_image_name`. Images should reduce
context misunderstanding, not expand the Summarizer's authority to judge results.
### 3.5 Existing Cold History
Complete steps, screenshots, and UI data already exist in DataEngine.
HistoryAnalyzer and Outputter offer partial retrieval, but Operator lacks one
unified direct history-recall tool.
### 3.6 Measured Baseline
Current identifiable Pro Operator calls in `traces/data_engine.db` show:
- 74 direct Operator model calls across 43 Operator turns;
- median input of about 10,221 tokens;
- input P90 of about 11,648 tokens and P95 of about 12,019 tokens;
- maximum input of about 12,747 tokens;
- median model-call latency of about 5.24 seconds and P90 of about 16.42 seconds;
- median full Operator-turn latency of about 20.02 seconds; and
- median observed step interval of about 17.15 seconds.
Input length and model latency show no reliable correlation in the observed
`8.8K-12.7K` range. That result must not be extrapolated to an `80K-100K`
dynamic context.
## 4. Design Principles
### 4.1 Separate Raw Records from the Model View
DataEngine stores complete facts. The prompt contains only the most valuable view
for the current turn. Compression changes what the model sees, never the source record.
### 4.2 Continuous Reasoning Before Compression
The complete previous reasoning, action expectation, action call, and Validator
result are required on the hot path and cannot be replaced by an ordinary summary.
### 4.3 Images Provide Context, Not Judgment
The Summarizer may inspect screenshots that actually exist for a step, but it
must preserve the current prompt's neutral responsibility. It may compress
intent, strategy, explicit verification, iteration progress, missing conditions,
and objective Failure Analyzer intervention. It must not independently decide
whether an action succeeded, failed, completed, or navigated to a new page.
A missing post-action screenshot means only that no independent post-action
visual evidence is available. It never means the screen was unchanged or the
action had no effect.
### 4.4 Plans Are Dynamic Indexes, Not History Keys
The current subgoal may increase relevance but must not control permanent
retention. History identity uses stable `step_id` values, time ranges, and source references.
### 4.5 Validator Facts Take Priority
```text
Explicit user instructions
> objective Validator or Controller results
> activity, package, XML, or OCR differences
> sourced content in visually grounded summaries
> Operator or Summarizer inference
```
### 4.6 Background Enhancements Cannot Become Dependencies
If a visual summary or history chunk is not ready, Operator immediately continues
with the existing detailed record.
## 5. Target Architecture
```text
+-----------------------------+
| DataEngine raw facts |
| steps / traces / images / UI|
+--------------+--------------+
|
Validator completes step ---------------+
|
+--------------v--------------+
| Step Memory Record |
| exact action/result/sources |
+-------+--------------+-------+
| |
immediate | asynchronous
| v
| +--------------------+
| | Context Summarizer |
| | decision image + |
| | optional post/UI |
| +----------+---------+
| |
+-------v----------------v-------+
| Pro Memory Store |
| hot history/summaries/chunks |
+---------------+---------------+
|
Perception current screenshot/XML -------+
v
+-----------------------------+
| Token-Budget Prompt Builder |
+--------------+--------------+
v
Operator
|
recall_history on demand
```
## 6. Layered History Model
### 6.1 L0: Current Observation
Every Operator turn receives the current screenshot, package/activity, compact
XML/OCR element list, screen dimensions and coordinate mapping, user-injected
instructions, active plan path, and background-task state. Historical images do
not enter normal requests.
### 6.2 L1: Hot History and Conversation Tail
```text
Previous AIMessage
- model-visible thinking / thought signature
- raw reasoning text
- turn-ending action tool call
Validator Function/Tool Result
- whether execution occurred
- execution state
- Failure Analyzer repair
- observed post-action state
Current HumanMessage
- current screenshot
- current XML
```
Keep one complete prior Operator turn normally and up to two when the last step
failed, remains uncertain, is recovering, or has an unfinished strategy. Do not
duplicate the same complete reasoning in `Execution History`. Until the provider
can preserve real messages and thought signatures, continue rendering raw/native
thinking verbatim.
### 6.3 L2: Visually Grounded, Neutral Step Summaries
Older steps use the current "summary plus exact action" structure, with a
multimodal summary input:
```text
Step N
Neutral contextual summary
Exact action
Validator outcome
Unresolved/recovery marker
```
The summary never replaces the exact action or overrides Validator. Screenshots
only clarify context; the output does not make a new result judgment.
### 6.4 Current Work Segment
The active milestone may organize recent summaries, loop and candidate progress,
unresolved items, and the most recent entry state. It remains a temporary label
that can change with the plan and never becomes a permanent history identity.
### 6.5 L3: Stable History Chunks
Long-term history is chunked by stable step ranges and token thresholds:
```text
Chunk 1: Steps 1-10
Chunk 2: Steps 11-19
Chunk 3: Steps 20-31
```
Close a chunk after roughly `1.5K-2K tokens` of step summaries or 8-12 steps.
Close early on a major package/activity change, user goal change, Failure Analyzer
boundary, complete strategy change, long work-segment pause, irreversible side
effect, or important external result.
Each chunk preserves its source range and step IDs, attempted path, verified
facts, failed paths, unresolved questions, important entities, entry and exit
states, and raw source references. Later plan rewrites do not alter chunk identity.
### 6.6 L4: Cold History
Retain every screenshot actually saved for a step, raw XML/OCR, raw/native
thinking, complete tool calls and results, ADB and log output, the full Failure
Analyzer trace, all summary versions, Notes, and plan revisions. Cold history is
excluded from normal prompts and loaded only through local matching or
`recall_history`.
## 7. Stable Data Model
### 7.1 StepMemoryRecord
```python
class StepMemoryRecord:
step_id: UUID
session_id: UUID
step_number: int
timestamp: datetime
plan_revision: str | None
current_subgoal_hash: str | None
operator_message_ref: str | None
thought_signature: str | None
raw_thinking_ref: str | None
native_thinking_ref: str | None
action: dict
validator_result: dict | None
failure_recovery: dict | None
pre_image_name: str | None
post_image_name: str | None
pre_ui_ref: str | None
post_ui_ref: str | None
ui_diff: dict | None
summary: str | None
summary_status: str
summary_model: str | None
summary_version: int
summary_decision_image_name: str | None
summary_post_image_name: str | None
summary_used_ui_diff: bool
```
The `summary_status` state machine is:
```text
pending -> visual_ready -> visual_failed -> stale
```
Exact actions, Validator results, and raw sources remain immediately available
regardless of summary state.
### 7.2 SummaryEvidenceMetadata
The summary remains one neutral natural-language paragraph. Deterministic source
metadata is stored separately:
```json
{
"step_id": "...",
"decision_image_name": "image-a",
"post_image_name": null,
"used_ui_diff": false,
"model": "gemini-3.7-flash",
"prompt_version": 2,
"summary_version": 3
}
```
The system derives these fields from actual inputs. A null `post_image_name`
expresses no action result or screen-change judgment.
### 7.3 HistoryChunk
```python
class HistoryChunk:
chunk_id: UUID
session_id: UUID
start_step_id: UUID
end_step_id: UUID
source_step_ids: list[UUID]
digest: str
verified_facts: list[str]
unresolved: list[str]
failed_paths: list[str]
important_entities: list[str]
entry_state: str | None
exit_state: str | None
model: str
version: int
```
Chunk summaries are versioned append-only records and never damage source summaries.
## 8. Visually Grounded Neutral Pro Summarizer
### 8.1 Inputs
Each background summary call uses only inputs that actually exist:
1. the decision screenshot seen by Operator, optionally annotated with action coordinates;
2. an independent post-action screenshot only when Validator captured and saved it;
3. the exact action name and parameters;
4. the Controller result;
5. the final Validator result;
6. Failure Analyzer recovery records;
7. available package/activity information;
8. available UI/OCR differences;
9. current-step thinking; and
10. a small number of recent summaries for loop and strategy continuity.
Do not resend the full plan and ten detailed steps. Build the message dynamically,
never substitute the decision screenshot for a missing post-action screenshot,
and never infer "no screen change" from missing post-action evidence.
### 8.2 Output Rules
`artemis/agents/summarizer/summarizer.md` remains the sole authority for summary
semantics. Preserve its philosophy and one-paragraph output. Use Operator's first
person to compress intent, strategy, explicit progress, and verified facts. Do
not independently claim success, failure, completion, non-completion, or page
navigation. A successful Controller return does not prove task success. Do not
guess unreadable values or judge outcomes by whether two images look similar.
Failure Analyzer intervention is summarized as an objective trigger and action,
without claiming that the repair worked. Step ID, evidence names, model, prompt
version, and summary version are system metadata rather than model-generated text.
The existing prohibition on outcome-judgment terms remains, including
`successfully`, `completed`, `entered`, `navigated to`, `failed`, `unsuccessful`,
`could not`, and `achieved`.
### 8.3 Model Selection
Quality-first default:
```text
Per-step summary: Gemini 3.7 Flash + available screenshots
History-chunk summary: Gemini 3.7 Flash
```
Single-quota, strict-isolation option:
```text
Real-time step summary: Gemini 3.5 Flash-Lite + available screenshots
Idle reprocessing for violations or important omissions: Gemini 3.7 + available screenshots
History-chunk summary: Gemini 3.7 Flash
```
The recommended quality ceiling remains 3.7 with available images. Flash-Lite is
a resource-isolation option, not a replacement for complex strategy, loop, and
recovery reasoning.
### 8.4 Scheduling and Isolation
- Operator never awaits Summarizer.
- Background summary concurrency is limited to one.
- Operator requests have absolute priority.
- Summary timeout or failure never affects graph execution.
- Existing detailed history remains the fallback.
- Prefer a separate endpoint, project, or quota.
- Without resource isolation, do not start a new 3.7 summary while Operator is active.
- Give non-preemptible background calls a short timeout.
- An optional final flush must not delay the next normal step.
## 9. Stable History Chunks
Milestones reflect a temporary interpretation of the task and are not stable fact
boundaries. A plan edit can otherwise hide relevant steps, split one path across
milestones, make rollback lose a prior strategy, or desynchronize summaries from
current labels. Subgoal identity therefore affects relevance only.
```python
if current_open_chunk.summary_tokens >= 1800:
close_chunk()
elif current_open_chunk.step_count >= 12:
close_chunk()
elif important_boundary_event:
close_chunk()
```
Prevent recursive summary loss by generating each chunk from source step
summaries, retaining `source_step_ids`, merging structured fact sets rather than
guessing from prose, and retaining every summary version.
## 10. Operator Cold-History Recall
Expose one unified tool:
```python
recall_history(
query: str,
step_range: list[int] | None = None,
include_details: bool = False,
include_images: bool = False,
max_results: int = 5,
) -> HistoryRecallResult
```
Search visually grounded summaries, history chunks, raw actions, Validator
results, Operator thinking, Notes, XML/OCR text, package/activity data, Failure
Analyzer traces, and image references. Phase one can use SQLite FTS, keywords,
step ranges, package/activity, and screen hashes without another model or vector database.
Return at most five results and 2K tokens by default. Every result includes a
step ID or number. Detailed output remains token-bounded. Image recall returns no
more than the images that actually exist for one step. Large raw results return
only relevant excerpts and references.
Operator should recall history when the current screen resembles an older state,
an old exact value is needed, two consecutive steps make no progress, the same
action is about to be attempted a third time, a summary conflicts with current
observation, the user asks to return to an earlier state, or Failure Analyzer
needs review. Ordinary steps must not recall history speculatively.
Before prompt construction, a local package/activity, screenshot-hash, and UI-
signature match may inject a short hint without a model call or historical image:
```text
Historical state hint: The current screen closely resembles the post-action
screen from Step 17. Use recall_history only if its details are needed.
```
## 11. Token Budget and History Selection
Recommended steady-state budgets:
```text
First Operator call target: 10K-12K
First-call soft limit: 12K
First-call emergency hard limit: 16K
Same-turn soft limit: 20K
Same-turn hard limit: 28K
```
```python
history_budget = (
operator_total_budget
- stable_system_tokens
- tool_schema_tokens
- current_observation_tokens
- active_plan_tokens
- same_turn_reserve
)
```
Always retain the previous complete Operator turn, current user constraints,
active plan path, unresolved items, recent failures and recovery, irreversible
actions or external side effects, the previous chunk's exit state, and current
loop or candidate progress.
Score optional history using current-subgoal relevance, unresolved status,
failure or recovery relevance, screen similarity, entity overlap, path boundary,
explicit user references, recency, resolved status, and duplication. Fill the
remaining budget by `value_density = priority / token_cost`.
When over budget:
1. Remove duplicate renderings.
2. Keep the previous real message and remove its duplicate detailed history text.
3. Replace older detailed steps with visual summaries.
4. Replace groups of old summaries with stable chunks.
5. Remove resolved, unrelated, low-value chunks.
6. Keep only relevant excerpts and references from large tool output.
Never evict hard user constraints, unresolved items, irreversible side effects,
critical values, recent recovery state, or the previous complete reasoning and expectation.
## 12. Prompt and Cache Layout
Keep the stable Operator role, safety rules, cognitive protocol, and output
contract in `SystemMessage`. Put the previous real AI message, thought signature,
action call, and Validator tool result in the conversation tail. Put the current
goal, active plan path, budgeted history, current screenshot, and UI elements in
the dynamic `HumanMessage`. Keep tool schemas and ordering stable when possible.
If the provider adapter supports stateful interactions, preserve one to three
recent Operator interactions with `previous_interaction_id`. Start a new
interaction when the token threshold is reached or the plan changes materially,
seeding it with the stable prefix and chunk summaries. Until then, retain the
current message-rebuild path.
## 13. Same-Turn Context Control
Large ADB, Notes, Explorer, log, or XML results can expand later model calls in
the same Operator turn. Store complete results in a trace or artifact and expose
a local envelope:
```json
{
"status": "success",
"important_lines": ["..."],
"errors": [],
"tail": ["..."],
"full_result_ref": "trace://...",
"truncated": true,
"original_chars": 87321
}
```
Apply character, line, and estimated-token limits; retain matched lines, errors,
and head/tail context; never call another LLM synchronously to summarize tool
output. Compress older results at the soft limit and require a decision instead
of accumulating more exploration at the hard limit.
## 14. Configuration
Add a dedicated Pro memory section instead of reusing Flash `step_summarizer`:
```jsonc
"agent": {
"pro": {
"memory": {
"enabled": true,
"preserve_previous_operator_turn": true,
"max_hot_turns": 2,
"operator_target_input_tokens": 12000,
"operator_hard_input_tokens": 16000,
"same_turn_soft_tokens": 20000,
"same_turn_hard_tokens": 28000,
"visual_step_summary": {
"enabled": true,
"model": "gemini-3.7-flash",
"thinking_level": "low",
"include_pre_image": true,
"include_post_image_when_available": true,
"include_ui_diff": true,
"max_concurrency": 1,
"operator_never_waits": true
},
"history_chunks": {
"enabled": true,
"max_steps": 12,
"target_source_tokens": 1800,
"model": "gemini-3.7-flash"
},
"recall": {
"enabled": true,
"max_results": 5,
"max_text_tokens": 2000,
"max_image_steps": 1
}
}
}
}
```
For a speed-first single-quota deployment, override only the visual step model
with `gemini-3.5-flash-lite`.
## 15. Implementation Scope
### 15.1 Operator
Update Operator, prompt builders, and configuration to add a token-budget memory
assembler, preserve recent real messages and Validator results, avoid duplicate
history rendering, move dynamic state out of the stable system prompt, remove
`<short_term_memory>`, add `recall_history`, inject local screen-similarity hints,
and enforce same-turn output limits.
### 15.2 Summarizer
Load decision and optional post-action images by stable step ID, include available
Validator and UI-diff data, send multimodal messages, preserve the neutral
one-paragraph contract, atomically update versioned evidence metadata, retain old
summaries on failure, enforce concurrency and timeout limits, and never block Operator.
### 15.3 Graph and State
Store the previous Operator `AIMessage`, thought signature, and Validator function
response. Remove the `short_term_memory` field while safely ignoring it in older
tasks. Read ready memory snapshots at the Perception/Operator boundary and retain
the current asynchronous Summarizer semantics.
### 15.4 DataEngine
Add summary status, version, evidence, thought-signature, message-reference, and
UI-diff fields; persist history chunks and local recall indexes; support atomic
updates by step ID; and preserve every older summary version and source.
### 15.5 Task Tree
Separate history visibility from rendering detail, render visual summaries and
stable chunks, use current subgoal only for relevance, avoid repeating the latest
step when it is already present in the real message tail, and retain exact actions
and validation results.
### 15.6 Memory Module
Suggested files:
```text
artemis/memory/pro_memory.py
artemis/memory/history_chunker.py
artemis/memory/history_retriever.py
artemis/memory/token_budget.py
artemis/tools/history_recall.py
```
Responsibilities include memory snapshots, value-based selection, chunk closure
and background summarization, FTS and screen-signature retrieval, the recall tool,
and provider-independent token estimation.
### 15.7 Configuration
Every capability must have an independent switch, default to current Pro fallback
behavior, tolerate missing configuration and older tasks, and support both the
3.7 quality mode and Flash-Lite isolation mode.
## 16. Delivery Phases
### Phase 0: Baseline and Observability
Freeze a representative long-task suite. Record input tokens, TTFT, model latency,
full-turn latency, summary-ready latency, failures, retries, history size,
duplicate-action rate, and Failure Analyzer activation without changing prompts.
### Phase 1: Protect and Improve Hot History
Preserve complete recent reasoning, remove the duplicate short-term-memory path,
avoid duplicate rendering, add the total token threshold, and locally truncate
large tool envelopes. Prove that speed does not regress first.
### Phase 2: Pro Visual Step Summaries
Load actual decision and optional post-action images by stable step ID while
preserving the neutral output contract and text fallback. Compare 3.7 with images,
Flash-Lite with images, and current 3.7 text-only behavior. Confirm no Operator wait.
### Phase 3: Stable History Chunks
Add milestone-independent rolling chunks, generate them from source summaries,
treat milestones as temporary working-set labels, and add source references and versions.
### Phase 4: Operator Cold-History Recall
Reuse step, screenshot, and UI search capabilities behind one bounded
`recall_history` tool, add local screen-similarity hints, and limit text and images.
### Phase 5: Real Multi-Turn Messages and Cache Optimization
Preserve real AI messages, tool calls, and thought signatures; separate stable
system and dynamic human content; evaluate stateful provider APIs; and safely
restart interactions at token thresholds. Begin only after the first four phases stabilize.
## 17. Test Plan
Unit tests must cover hot-history preservation, removal of short-term-memory
parsing, action/Validator pairing, duplicate suppression, two-turn recovery,
image lookup by step ID, optional post-image behavior, neutral-output validation,
Validator precedence, concurrency, timeout, zero-wait behavior, chunk thresholds,
source traceability, recall bounds, image limits, and hard token protections.
Integration tests must include tasks longer than 30 steps, repeated similar
screens, plan rewrites, cross-app flows, dialogs, Toasts, WebViews, Canvas,
Failure Analyzer recovery, repeated ADB exploration, long lists, monitoring loops,
summary timeout or quota exhaustion, and exact-value recall from older screenshots.
Run blinded evaluations on real traces comparing 3.7 text-only, Flash-Lite with
available images, and 3.7 with available images. Reviewers score fidelity to
intent and explicit facts, unsupported visual claims, unauthorized outcome
judgments, anomaly retention, next-step usefulness, generation time, and ready latency.
## 18. Acceptance Criteria
Under the same device, task, model, and comparable service load:
- first-call input P50 increases by no more than 3% and P95 by no more than 5%;
- Operator model-call and full-turn P50/P90 latency regress by no more than 5%;
- observed step-interval P50/P90 regress by no more than 5%;
- Operator waits for summaries exactly zero times;
- normal Operator requests contain zero historical images; and
- images returned by `recall_history(include_images=True)` count only as explicit recall.
Run at least 30 paired tasks and report confidence intervals. Quality must not
regress for previous-strategy recall, overall task success, or Failure Analyzer
recovery. Visual-fact accuracy, important-fact retention across plan edits,
repeat-action rate, history-induced rollback rate, and top-five recall hit rate
must improve. Summary/Validator conflict should approach zero.
## 19. Failure Modes and Fallbacks
- If a visual summary is pending, use complete previous reasoning, exact action, and Validator result.
- If visual summarization fails, set `visual_failed`, preserve old summaries, and do not block for retries.
- If screenshot evidence is incomplete, include only actual evidence and never infer the missing post-action state.
- If chunk generation fails, keep source summaries and retry the pending chunk during an idle window.
- If recall output is too large, return high-scoring excerpts and step/artifact references.
- If background work competes with Operator, switch to Lite, pause new summaries, or use a separate quota.
- If stateful interactions are incompatible, return to prompt reconstruction while preserving raw/native thinking.
## 20. Rollout Flags
```text
pro_memory_enabled
preserve_operator_message_tail
visual_step_summary
history_chunks
operator_history_recall
stateful_operator_interactions
```
Recommended order: enable metrics and token thresholds; run visual summaries in
shadow mode; use them after blinded validation; enable stable chunks; enable
`recall_history`; then evaluate real multi-turn interactions. Disabling any flag
must restore current Pro behavior.
## 21. Improvements Over the Current System
| Capability | Current Pro | Redesigned Pro |
|---|---|---|
| Previous reasoning | Re-rendered as detailed history | Preserved and ready for a real message/thought-signature chain |
| Short-term strategy | Optional `<short_term_memory>` | Carried by the complete previous message, Validator, plan, and Notes |
| Step summary | 3.7 reads text only | Neutral 3.7 summary reads actual available images, UI, and Validator data |
| Exact action | Retained beside summary | Still retained and never replaced by visual summary |
| History pruning | Current subgoal plus latest three steps | Working set plus stable chunks and relevance budget |
| Plan rewrite | May change old-step visibility | Stable step/chunk references survive label changes |
| Old screenshots | No direct Operator recall | One actual step can be recalled on demand |
| Cold history | Primarily for HistoryAnalyzer/Outputter | Locally searchable and bounded for Operator |
| Context control | Mainly a step window | Total token budget, value density, and same-turn limits |
| Cache use | Highly dynamic system prompt | Stable system prefix separated from runtime state |
| Background failure | Summary may be delayed | Explicit state machine and zero-wait fallback |
The objective is not to display more history. It is to preserve recent reasoning,
give older summaries real visual context, decouple long-term identity from plan
milestones, make compressed records precisely recallable, and keep normal input
size and critical-path model-call count unchanged.
## 22. Recommended Defaults
```text
Normal Operator input: 10K-12K tokens
First-call soft/hard limits: 12K / 16K
Same-turn soft/hard limits: 20K / 28K
Complete hot turns: 1 normally, up to 2 for failure/uncertainty
Current-work summaries: budget-controlled, not a fixed count
History chunk: 8-12 steps or about 1.8K source-summary tokens
Historical images in normal input: 0
Summary input images: 0-1 decision, 0-1 independent post-action
Explicit recall images: actual images from at most one step
Preferred step-summary model: Gemini 3.7 Flash + images
Single-quota speed mode: Gemini 3.5 Flash-Lite + images
History-chunk model: Gemini 3.7 Flash
Background summary concurrency: 1
Additional critical-path calls: 0
Critical-path summary waits: 0
```
## 23. Final Decision
Do not remove previous reasoning and replace detailed history with an image-free
capsule. Preserve complete previous-turn reasoning, upgrade older steps to
visually grounded neutral summaries, use milestone-independent rolling chunks,
allow bounded cold-history recall, and enforce a fixed token budget.
The first, highest-value implementation slice is:
1. preserve complete previous reasoning and remove the duplicate short-term-memory path;
2. add the decision screenshot and optional actual post-action screenshot to the current neutral Pro Summarizer;
3. bind summaries with stable step IDs; and
4. establish speed baselines and blinded visual-summary evaluations.
Do not expand the normal history window or remove the current detailed-history
fallback until all four items have passed validation.
-606
View File
@@ -1,606 +0,0 @@
# ARTEMIS Video Analysis Memory and Reliability Design
Status: implemented (2026-08-25)
Audience: ARTEMIS runtime, perception, tracing, and mobile automation maintainers
## 1. Executive summary
The current video analyzer treats its blackboard as state owned by one
`VideoAnalyzer` object. Tool invocations create new analyzer objects, so the
state is lost between calls. A failed parent analysis also discards useful
work completed by child analyzers. Overlap detection is advisory and does not
prevent the same interval from being sent to a video model again.
This design moves video memory out of the agent and into a video-scoped,
persistent blackboard. The blackboard is the source of truth for:
- which intervals have been analyzed for a normalized query and modality;
- observations and evidence extracted from those intervals;
- successful, running, and failed segment attempts;
- resumable partial results across tool calls;
- concurrency leases that prevent duplicate model requests.
The analyzer becomes a coordinator over this state. It calculates the set
difference between a requested interval and completed coverage, analyzes only
the gaps, commits every successful chunk immediately, and returns partial
results without erasing prior work.
The first implementation deliberately uses SQLite and the existing trace
directory. Vector search, semantic graphs, provider-managed context caches,
and entity re-identification are future layers, not prerequisites for fixing
correctness and reliability.
## 2. Goals
### 2.1 Functional goals
1. Share analyzed intervals and results across every `VideoAnalyzer` created
for the same recording.
2. Avoid repeating an identical successful `(video, interval, query,
modality)` analysis.
3. Persist each successful chunk before sibling chunks or the parent agent
finish.
4. Preserve successful chunks when some chunks fail.
5. Expose previous observations to both native Gemini and universal-model
coordinators.
6. Keep evidence tied to recording-relative timestamps and durable files.
7. Prevent concurrent duplicate requests with an expiring lease.
8. Maintain existing public tool signatures.
### 2.2 Reliability goals
- A process or model failure may lose only the currently running chunk.
- An expired running lease is reclaimable.
- Retrying a request analyzes only uncovered or failed chunks.
- A partial result explicitly identifies failed intervals.
- SQLite writes are idempotent and safe under multiple analyzer instances.
- Evidence used by persistent memory is not deleted by temporary-file cleanup.
### 2.3 Non-goals for phase one
- Semantic similarity reuse between materially different queries.
- Cross-recording memory or user identity memory.
- Replacing Gemini or the configured universal VLM.
- Hosting an open-weight video model or accessing model KV caches.
- Full WorldMM semantic graphs, HippoRAG, VLM2Vec, or ReflectWorld ReID.
- Permanent retention of uploaded provider files.
## 3. Current failure modes
### 3.1 Agent-owned state
`VideoAnalyzer.__init__` initializes an empty list. `video_tool.py` creates a
new analyzer for every tool call. Consequently, a later invocation cannot see
the earlier invocation's ledger.
### 3.2 Advisory overlap handling
The analyzer calculates high-overlap warnings but continues with extraction,
compression, upload, and inference. A warning in a prompt is not an
idempotency mechanism.
### 3.3 Commit-at-the-end behavior
Some event observations are appended during child analysis, but they are only
in memory. If the parent fails or cleanup runs, the trace can contain expensive
model activity with no reusable result.
### 3.4 Mutable recording ambiguity
An open-ended interval is currently cached as `(start, None)`. The meaning of
`None` changes while recording continues, so this cannot be a stable cache
identity.
### 3.5 Disposable evidence
Extracted screenshots live in temporary directories registered for cleanup.
The ledger may therefore reference files that no longer exist.
## 4. Design principles
### 4.1 Memory before reasoning
Perception results are written before the coordinator generates its final
answer. Answer generation reads stored evidence; it is not the owner of that
evidence.
### 4.2 Evidence-backed records
Every observation carries an interval, query, confidence, modality, model
metadata where available, and an optional durable screenshot. Summaries never
stand alone without temporal provenance.
### 4.3 Objective observation versus query result
The design has two logical layers:
- **Observations** describe visible or audible events and can help later
queries.
- **Segment results** answer one normalized query over one complete interval
and define exact reusable coverage.
An observation about a button is useful context for many questions, but a
segment result for "did login succeed" must not automatically satisfy a
different query such as "how long did the spinner remain visible".
### 4.4 Immutable interval identity
An analysis segment always has numeric start and end timestamps. Open-ended
requests are resolved to the safe available end before they are claimed or
committed.
### 4.5 At-least-once execution, idempotent commit
Provider requests can be retried. Database constraints and deterministic keys
make commits idempotent. Exact-once provider execution is not promised, but
duplicate execution is strongly suppressed by leases.
## 5. Architecture
```text
video_analyzer tool
|
v
VideoAnalyzer coordinator ---------> persistent blackboard ledger
| | |
| | +-- observations/evidence
| +-- segment coverage and attempts
|
+-- resolve stable numeric interval
+-- subtract completed coverage
+-- claim uncovered chunks with leases
+-- run child analysis
+-- commit each successful chunk immediately
+-- mark failed chunks retryable
+-- synthesize cached + new + partial results
```
The blackboard is acquired from `ArtemisContext`, not constructed as private
agent state. Multiple analyzers in the same context share the object, while
SQLite makes the data available to later instances and process restarts.
## 6. Recording identity
The preferred blackboard key is the active `RecordingSession.video_id`:
```text
video:<uuid>
```
If no active recording is available, the fallback is the DataEngine session:
```text
session:<uuid>
```
The context retains the resolved blackboard instance so analysis after a
recording stops continues to use the same key. Extracted recording artifacts
also carry an explicit source revision:
```text
<video-uuid>:<generation>:<sealed-end>
```
A generation advances whenever the recorder rolls a physical segment. The
recorder tracks the sealed numeric prefix, and every `end=None` analysis is
resolved to the current safe numeric end before it is claimed.
## 7. Persistence model
### 7.1 `video_analysis_segments`
One row represents a complete query-specific analysis of a numeric interval.
| Column | Meaning |
| --- | --- |
| `board_key` | Recording/session namespace |
| `query_key` | SHA-256 of the normalized query |
| `query_text` | Original query for debugging and retrieval |
| `modality` | `video` or `audio` |
| `start_ms`, `end_ms` | Stable integer interval |
| `status` | `running`, `succeeded`, `retryable_failed`, `permanent_failed` |
| `lease_owner`, `lease_expires_at` | Duplicate-work suppression |
| `attempt_count` | Number of acquired attempts |
| `summary`, `analysis` | Reusable completed result |
| `error` | Last structured failure text |
| `created_at`, `updated_at` | Audit timestamps |
The unique key is:
```text
(board_key, query_key, modality, start_ms, end_ms)
```
### 7.2 `video_analysis_observations`
One row represents a timestamped event or fact.
| Column | Meaning |
| --- | --- |
| `observation_id` | Deterministic content fingerprint |
| `board_key` | Recording/session namespace |
| `start_ms`, `end_ms` | Event interval |
| `query_text` | Query that caused extraction |
| `summary` | Concise observation |
| `confidence` | Model-reported confidence |
| `modality` | `video` or `audio` |
| `screenshot_path` | Durable evidence path |
| `extra_json` | Forward-compatible metadata |
| `created_at` | Audit timestamp |
The deterministic observation ID makes repeated commits safe.
### 7.3 Evidence layout
```text
<trace-session>/video_blackboard/evidence/<content-id>.jpg
```
Temporary screenshots are copied into this directory before their source is
registered for deletion. Stored records reference the durable copy.
## 8. Segment state machine
```text
lease expires
running ------------------------------+
| |
| success v
+----------> succeeded retryable_failed
| |
| retryable error | claim again
+----------> retryable_failed ------+
|
| non-retryable/budget exhausted
+----------> permanent_failed
```
Claim behavior is transactional:
1. A missing row is inserted as `running`.
2. `succeeded` returns its cached result without acquiring work.
3. `running` with a live lease returns `in_progress`.
4. An expired lease or retryable failure is atomically reclaimed and increments
`attempt_count`.
Exhausted failures store a normalized category and category-derived
retryability. Permanent failure is reserved for validated bad input,
authentication/policy errors, cancellation, or explicit decisions.
## 9. Coverage and planning
Coverage is query- and modality-specific. Given requested interval `R` and
the union of successful intervals `S`, the work planner calculates:
```text
missing = R - union(S)
```
Each missing interval is split into bounded chunks. Completed chunks are
included in the returned result; only gaps are sent to the provider.
Interval math uses integer milliseconds internally to prevent floating-point
key drift. API-facing timestamps remain seconds.
Observations from other queries are still placed in the ledger as context, but
they do not count as completed coverage for the new query.
## 10. Execution flow
### 10.1 Tool invocation
1. Acquire the context-scoped blackboard.
2. Refresh the local ledger from persistent observations.
3. Give the coordinator the full ledger snapshot.
4. Coordinator requests segment analysis.
### 10.2 Child analysis
1. Resolve a numeric interval from recording metadata.
2. Calculate missing intervals.
3. For every chunk, acquire a lease.
4. Reuse `succeeded`; skip live `running`; execute only `claimed`.
5. Extract/compress/upload and invoke the configured VLM.
6. Persist timeline observations as soon as they are parsed.
7. Persist the complete segment result before returning to the parent.
8. On final failure, mark that exact chunk failed and keep all sibling results.
### 10.3 Parent synthesis
The parent receives cached and newly completed results in the same format. If
some chunks failed, it receives an explicit partial-result marker and failed
intervals. It may answer with qualified confidence or request a targeted retry.
## 11. Retry and degradation policy
The desired production order is:
1. retry transient upload/poll/generation failures with exponential backoff
and jitter;
2. retry the exact chunk without expanding its identity;
3. split a repeatedly failing chunk into smaller children;
4. use the configured fallback video model;
5. fall back to high-frequency keyframes, OCR, and UI hierarchy around known
action timestamps;
6. return partial evidence with machine-readable failed intervals.
The runtime implements this order with structured failure classes, bounded
bisection, configured fallback models, and a primary-model circuit breaker.
## 12. Provider and sampling strategy
Provider context caching is an optimization, not the system of record. ARTEMIS
must remain correct if a provider cache misses or expires.
Default 1 FPS video sampling is supplemented by exact, dense seeks around
DataEngine action timestamps. Quiet intervals remain sparse while taps,
transitions, toasts, and spinners receive a higher sampling budget.
## 13. Concurrency
- SQLite uses WAL mode and short `BEGIN IMMEDIATE` claim transactions.
- The unique segment key prevents duplicate rows.
- A UUID lease owner identifies one execution attempt.
- Leases expire so crashes do not permanently block work.
- Existing API and transcode semaphores still bound local concurrency.
- Conflict cleaning affects an analyzer's presentation ledger; it never
deletes persistent observations written by another analyzer.
## 14. Cleanup and retention
Three artifact classes have different policies:
1. Trimmed/compressed chunks: temporary, deleted after the call.
2. Provider-uploaded files: temporary, deleted best-effort after inference.
3. Blackboard evidence: durable for the trace/session retention period.
Deleting a trace/session removes blackboard rows through DataEngine and
evidence through the session directory lifecycle.
## 15. Observability
Required counters and trace fields:
- requested duration;
- cached duration;
- newly analyzed duration;
- number of claimed, cached, in-progress, succeeded, and failed chunks;
- provider attempts and error class;
- blackboard key and query key;
- partial versus complete outcome;
- evidence paths attached to observations.
No prompt or trace status should report success merely because an error string
was returned as normal text.
## 16. Compatibility and migration
- Public `video_analyzer` and `video_analyzer_pure` arguments do not change.
- Existing in-memory ledger dictionaries remain the presentation format.
- When DataEngine persistence is unavailable, the blackboard falls back to a
context-scoped in-memory store. This preserves tests and embedded usages but
does not promise process-restart recovery.
- Databases are migrated with `CREATE TABLE IF NOT EXISTS`; no destructive
migration is required.
- Existing traces without blackboard rows continue to work and start empty.
## 17. Security and privacy
- Evidence remains local under the trace directory.
- Provider files are deleted best-effort after requests.
- Blackboard rows must not store credentials or entire model prompts.
- Retention follows trace retention.
- Future cross-session or identity memory requires a separate consent and
privacy design; it is outside this scope.
## 18. Testing strategy
### 18.1 Unit tests
- persistence across two blackboard instances using the same SQLite file;
- deterministic observation deduplication;
- interval union and missing-range calculation;
- exact successful segment reuse;
- live lease suppression and expired lease reclamation;
- failed chunk remains retryable;
- evidence is copied to durable storage;
- two `VideoAnalyzer` instances share prior ledger state;
- repeated `exec_spawn_sub_agent` does not invoke child inference again;
- mixed success/failure returns partial output and preserves success.
### 18.2 Integration tests
- analyze overlapping intervals against a fixture recording;
- interrupt after one successful chunk and resume;
- concurrent identical requests result in one provider call;
- active recording resolves open-ended requests to numeric endpoints;
- evidence remains readable after analyzer cleanup.
### 18.3 Device tests
Per ARTEMIS mobile testing rules, end-to-end mobile flows must be explored and
verified on a connected device before production test scripts are finalized.
The refactor's deterministic unit tests do not require a device; live timing,
fast UI transitions, and recording alignment do.
## 19. Rollout
1. **Complete:** SQLite blackboard, sharing, observation persistence, exact
coverage reuse, leases, and tests.
2. **Complete:** failure classification, jitter, bounded bisection, fallback,
and circuit breaking.
3. **Complete:** recording generations, sealed-prefix metadata, and
generation-scoped caches.
4. **Complete:** action-aware sampling and real audio delivery.
5. **Deferred optimization:** provider file/context caching, gated by an
explicit retention policy.
6. **Future accuracy layer:** semantic/event hierarchical retrieval after
production traces provide a representative evaluation corpus.
## 20. Acceptance criteria
- Two analyzer instances in one task see the same ledger.
- Repeating an identical successful interval/query causes zero new child VLM
calls.
- If one of several chunks fails, successful chunks are available to the next
invocation and are not rerun.
- A crashed `running` chunk becomes reclaimable after lease expiry.
- Persistent evidence survives normal analyzer temporary-file cleanup.
- Existing video-analyzer unit tests and new blackboard tests pass.
Additional reliability criteria:
- timeout/media failures recover through bounded recursive subdivision;
- provider-wide failures do not trigger request fan-out;
- primary-model failures select the configured `utils.video_analyzer` fallback;
- the primary circuit opens after a configured consecutive-failure threshold;
- audio analysis is independently resumable and reusable;
- recording generation changes invalidate stale trim-cache identities;
- frames are sampled densely around recorded mobile actions;
- traces include blackboard status, attempts, failure categories, successful
duration, observation count, and circuit state.
## 21. Implemented production architecture
The completed refactor extends the phase-one blackboard with the following
production controls:
1. Failures are normalized as rate limit, provider outage, timeout,
connection, authentication, bad request, media processing, cancellation,
or unknown. The class determines retryability, bisection, and fallback.
2. Timeout, media, oversized, and unknown failures can recursively bisect a
chunk down to configured size/depth bounds. Provider outages, connections,
and rate limits do not bisect because fan-out would amplify load.
3. Coordinator, video child, and audio child use the configured utility-model
fallback. A context-scoped circuit breaker bypasses an unhealthy primary.
4. Recording results expose `video_id`, `generation`, `sealed_until`, and
`source_revision`. Trim caches are scoped by video UUID and generation;
open-ended requests are resolved to numeric safe ends.
5. Universal video analysis receives sparse baseline frames, dense frames
around DataEngine mobile-action timestamps, and the real audio track.
Audio-only analysis sends a standard base64 audio content block. Models
that reject audio degrade only that video chunk to visual-only analysis.
6. Audio uses the same lease, exact reuse, completion, and failure state
machine as video.
7. DataEngine session deletion removes blackboard rows by session/video key;
evidence is deleted with the trace directory. Universal-path temporary
artifacts are cleaned on early exit.
The persistence schema adds `error_category`, `model_name`, and
`source_generation` to every segment attempt. Migrations are additive and
safe for existing SQLite traces.
## 22. State-of-the-art review and adoption decision
The relevant frontier is not “send the full video to a bigger model.” Recent
systems converge on four architectural ideas:
1. **Agentic coarse-to-fine localization.** [VideoAgent](https://arxiv.org/abs/2403.10517)
iteratively retrieves a small number of frames; [VideoTree](https://arxiv.org/abs/2405.19209)
builds a query-adaptive temporal hierarchy.
2. **Persistent hierarchical multimodal memory.** The memory-augmented
[VideoAgent](https://arxiv.org/abs/2403.11481) separates memory construction
and query-time tool use. [VideoARM](https://arxiv.org/abs/2512.12360) uses an
observe-think-act-memorize loop over hierarchical memory.
3. **Adaptive redundancy reduction.** [LongVU](https://arxiv.org/abs/2410.17434)
removes redundant frames and preserves query-relevant temporal/spatial
detail instead of sampling uniformly.
4. **Retrieval over very long media.** [VideoRAG](https://arxiv.org/abs/2502.01549)
combines graph/text grounding with multimodal retrieval. This is useful for
many-hour corpora, but heavier than one mobile-test recording.
Provider guidance supports the same separation: Gemini's
[video-understanding documentation](https://ai.google.dev/gemini-api/docs/video-understanding)
describes direct audio/video processing and reusable Files API inputs, while
[context caching](https://ai.google.dev/gemini-api/docs/caching) is an
optimization for repeated common input. Neither is a durable work ledger.
### 22.1 Adopted directly
- VideoARM/VideoAgent's observe → reason → act → memorize loop;
- query-scoped coarse-to-fine temporal analysis;
- separation of objective observations and answered query spans;
- adaptive sampling, with mobile action timestamps as the domain-specific
relevance signal;
- independent visual/audio tools and evidence-grounded synthesis.
### 22.2 Not copied wholesale
The research implementations assume offline benchmark videos, often require
GPU feature extractors, and optimize multiple-choice VideoQA. ARTEMIS operates
on a growing recorder stream, must survive provider/process failures, prevent
duplicate paid calls, and tie evidence to test-relative action time. Importing
VideoARM or VideoRAG would add a second orchestration runtime without solving
leases, idempotent commit, active-recording identity, retention, or mobile
alignment.
ARTEMIS therefore ports their architectural invariants, not their
benchmark-specific code, and supplies the production controls the prototypes
do not provide.
## 23. Runtime configuration
All controls live under `video_analyzer` and flow through the SDK builder:
| Setting | Default | Purpose |
| --- | ---: | --- |
| `chunk_size_seconds` | 60 | Initial coarse chunk size |
| `min_chunk_seconds` | 4 | Smallest bisection leaf |
| `max_split_depth` | 4 | Bound failure-driven fan-out |
| `circuit_breaker_threshold` | 3 | Failures before primary circuit opens |
| `circuit_breaker_cooldown_seconds` | 60 | Half-open delay |
| `action_window_seconds` | 2 | Dense window on either side of an action |
| `dense_action_fps` | 4 | Sampling rate in action windows |
| `max_dense_action_frames` | 24 | Dense-frame budget per chunk |
| `native_max_retries` | 1 | Native-provider retries before universal fallback |
| `model_call_timeout_seconds` | 120 | Hard bound for one model response |
The configured `llm_config.utils.video_analyzer.fallback` supplies the model
fallback. Public video tool signatures remain unchanged.
## 24. Verification status
Local verification covers persistence, exact reuse, leases, partial commit,
structured failures, circuit breaking, fallback selection, timeout bisection,
audio reuse, real audio content blocks, action-aware sampling, exact frame
seeking on an MP4 fixture, generation-scoped caches, configuration
propagation, and existing analyzer/controller behavior.
### 24.1 Real-device acceptance (2026-08-25)
A connected Pixel 10 (`59260DLCR00360`) completed a deterministic Google
Calculator recording with independently observed UI ground truth:
- `12+34=46` at 21.15s;
- `7×8=56` at 32.44s;
- history opened at 39.01s and closed at 42.12s;
- the final screen returned to result `56`.
Three query-independent video analyses all matched that ground truth. Each
query was then replayed twice with the identical interval and query. All six
replays returned `CACHED VIDEO ANALYSIS` in roughly 3ms, with no new child
analysis. Final blackboard state was three succeeded segments, 127.8 covered
query-seconds, and eight timestamped observations. The machine-readable
report and retained MP4 are produced by
`scripts/real_device_video_acceptance.py`.
The run also exercised real provider degradation: native Gemini returned a
503 and later stalled a stream. The affected query preserved prior successes,
retried only its own uncovered work, then completed through the configured
universal fallback. Based on that trace, native attempts now default to two
total calls with a 120-second response bound before fallback.
Device exploration also exposed two non-video runtime faults that affect
perceived stability and were fixed as part of the gate:
1. UIAutomator may return current string/dictionary bounds with a stale cached
center. Dynamic taps now always derive their center from fresh bounds and
use the cached center only when bounds are unavailable.
2. A stale desktop IPC port previously blocked every DataEngine event for one
second. IPC connects now use a short localhost timeout plus exponential
backoff, and detached agent initialization has a 30-second hard bound.
The acceptance result demonstrates 100% accuracy and reuse for this defined
fixture and run. It is not a mathematical guarantee for every future app,
video, device, or provider outage; production SLOs require a larger repeated
corpus and ongoing monitoring.
-370
View File
@@ -1,370 +0,0 @@
# Checker 全流程验证重构(计划驱动检查点 + 乐观异步 + 出口结算)
> 交接文档:本计划自包含,执行者不需要任何前置对话上下文。
> 日期:2026-08-30(v2.1 实施版)。文中行号以当日工作区为准,**执行前一律先按内容搜索确认,行号可能漂移**。
---
## 0. 工作区纪律(先读)
- 本工作区可能有**多个并发 Claude 会话**在改代码,工作树里有大量未提交改动(部分不是你产生的)。
- **严禁 `git stash`**(会卷走别人的半成品)。
- 需要对比基线用 `git show HEAD:<path>` 或临时 worktree,不要动工作区里已有的未提交修改。
- 不要 commit/push,除非用户明确要求。
- 每个 Work Package 完成后跑对应的单测(见 §8),基础设施类测试可用 `ARTEMIS_FAKE_LLM=1` 环境变量走假 LLM。
---
## 1. 背景与结论(已定,不要重新设计)
### 1.1 旧中途 Checker 的病根(仍然成立,全部要摘除)
1. **Checker 失败的 turn 不写入 DataEngine 历史**——`graph.py` `execution_check_node` 里 `if checker_success:` 才 `record_step`。
2. **notes 目录全量快照回滚**——Operator 每轮前全量快照,checker 失败整体恢复。
3. **Prompt 整体切换**——存在 checker feedback 时换 `troubleshooter_template` + verification chat 多轮辩论。
4. **僵尸 checker**——`ctx.checker_task` 单槽位覆盖不 cancel,副作用在任务体内。
**关键认知:病根是"回滚过去、切换模板、副作用失控",不是异步验证本身。**
### 1.2 目标架构与能力边界
系统承担**测试任务**,短暂状态只能在发生时刻附近取证,所以中途检查点必须存在,以**乐观异步**方式运行。架构分四层:
- **配置层**:`中途检查` 与 `结尾终审` 两个独立开关(§6)。用户关闭的检查一律尊重;**未执行的断言报告为 unchecked,绝不计为通过**。
- **计划层**:Planner 把检查项声明进 task_plan.md(`- verify:` / `- assert:`,带时机,§4)。计划文件是唯一权威载体(锚定子目标、随重写存活、受守护覆盖);但**相关 prompt 注入全部条件化**(§4.4)——检查全关时 Planner 不生成 check 行、语法说明不进任何 prompt,零上下文污染。
- **能力层**:Checker 享有与 Operator 等价的观察能力(历史、截图、当前屏幕),但工具权限独立——只读检索 + 受约束探测,零副作用,只出裁决(§5.4)。
- **控制层**:Graph 识别检查点、spawn 任务、收割裁决、执行副作用、出口结算(§5.1-5.3)。
**能力边界(必须如实声明,写进 planner prompt 与最终报告语义)**:
- 本期的中途检查是**事后核验**:它能在事后发现流程违规并留下证据,**不能阻止**违规发生。"必须确认 A 之后才允许执行 B"这类**强制前置关卡本期不支持**——Planner 遇到此类用户要求时不得静默接受:断言照常生成(保留用户原语义),但计划与报告中标注"顺序核验为事后判定"。
- 异步检查只能核验**已被采集的证据**。从未被记录的瞬时提示无法找回——瞬态断言的证据来源是执行历史中的前后截图;Planner 应优先把预期表述为可持久探测的状态。
**三条铁律**:
1. Operator 的历史与上下文 **append-only**——不换 prompt、不回滚 notes、不丢 step、不开平行对话。
2. 验证影响未来(打回状态、注入 finding),**不修改过去**。
3. **放行、裁决、任务收尾三者分离**:fail-open 只影响放行决策,裁决原值(inconclusive)永远入账;assert 失败不修复但必须产生机器可读的测试失败结果并传导到 SDK 收尾(§5.5)。
**裁决 append-only**:每次检查有独立 `attempt_id`,所有裁决(含被 supersede 的)永久入账;断言首次失败不被覆盖;报告呈现每条断言 passed / failed / inconclusive / unchecked 与完整裁决序列。
### 1.3 横切原则:Prompt 与工具按配置装配(No Dead Instructions)
本次全部改动遵守同一条装配纪律,**任何 agent 的 prompt 段与工具注册都是"配置 × 当前场景"的函数**:
1. 配置关闭的能力,不得在任何 prompt 中被提及,其工具不得注册——不存在"指令还在、机制已关"的死指令;
2. 反向同样成立:prompt 中出现的每条规则,必须对应一个当前激活的机制(如"删除会被恢复"只在守护生效时可说);
3. 装配点集中在代码里的组件列表/工具注册表(PromptComponent 列表、工具 dict 构造函数),**不在 prompt 模板内部堆条件文本**;
4. 场景也参与装配:同一 agent 的不同入口(如 checker 的中途/终审)按各自的证据纪律装配不同的工具与指南段。
落点:Planner(§4.1/§4.2 条件语法段与生成指令)、Operator(§4.4 说明段、§3.1 feedback 组件按内容渲染)、Checker(§5.4 两入口装配表)、SDK 返回结构(§5.5 test summary 仅在存在 check 项时出现)。
---
## 2. 代码地图(改动涉及的锚点)
Graph 拓扑(`artemis/graph/graph.py` `get_graph`,~622):
```
START → planner → convergence
perception → operator → execution_check ─(execute_decisions)→ validator → summarizer → convergence
└(review_subgoals)→ convergence
convergence ─(continue)→ perception
└(end)→ END
```
注意时序:Operator 在自己的 turn 内通过 note 工具写计划(含完成标记);该 turn 的设备动作在**其后的 validator 节点**才执行;`execution_check_node` 的 `record_step` 记录的是本 turn 的观察(pre 截图)与决策。因此"子目标完成"的证据 = 截至本 turn step 的历史(上一 turn 的 post 截图 + 本 turn 的 pre 截图)。
| 锚点 | 位置(按内容搜索确认) | 处置 |
|---|---|---|
| `execution_check_node` | `graph.py` ~88-289 | 重构(§3.2、§5.1、§5.2) |
| checker 中途触发 ①(chat_path 分支) | ~100-132 | 删除 |
| checker 单槽消费段 | ~134-153 | 删除 |
| planner validation 消费段 + 拒绝提前 return | ~155-211,**return 在 ~190** | 保留机制;拒绝分支补 record_step(§3.2) |
| record_step(`if checker_success:` 门控) | ~213-250 | 无条件化(§3.2) |
| notes 快照 commit/rollback 段 | ~252-280 | 删除 |
| checker 中途触发 ②(`new_completions`) | `_process_plan_write` 末段 ~499-506 | 改为排队 pending checkpoint(§5.1) |
| planner validation 漂移检测 | `_process_plan_write` `milestones_changed`(~452) | 保留;旁加确定性 check 行守护(§4.3) |
| `convergence_gate` | ~625 | 扩为三值路由(§5.3) |
| notes 快照创建 / troubleshooter 分支 / verification chat 机制 | `operator.py` | 删除(§3) |
| `CheckerFeedbackPromptComponent` | `operator/prompts.py` ~231 | 改写为读 state 的 append 注入组件 |
| checker 本体 | `agents/checker/checker.py` `run_async_check`(~80)、`CheckerResult`(~68) | 重写(§5.4) |
| context 字段 | `context.py`:`checker_task`、`disable_checker=True`(~108)、`checker_max_chat_rounds`(~110)、`disable_planner_validation=True`(~111,**注意默认关**)、`task_plan_snapshot` | §3、§6 |
| state 字段 | `graph/state.py`:`checker_success`、`initial_goal` | 保留;新增字段见 §5.2/§5.5 |
| plan 语法 | `utils/plan_grammar.py`:`CHECKBOX_LINE_RE`(~49)、`parse_plan`(~149)、`PlanSnapshot`(~100)、`subgoal_hash`(~56)、`milestones_changed`(~167)、`new_top_level_completions`(~172)、`PLAN_GRAMMAR_SPEC`(~199) | 扩展(§4.1) |
| DataEngine | `engine.py`:`record_step`(~491,存 pre/post 截图)、`get_step_number`(~922)、`get_agent_friendly_steps`(~1045)、`end_session(status)`(~300);`storage.py` `get_steps`(~651) | 只加只读检索辅助 + 收尾状态核对(§5.5) |
| SDK 收尾 | `sdk/agent.py`:图完成后无条件 success 日志 + `end_session("completed")`(~718-721)、task.status 收敛(~1035-1040)、trace 命名 `_PASS/_FAIL`(~1061) | 接入 run outcome(§5.5) |
| verification 工具函数 | `utils/verification.py` | 删除 |
| 快照工具函数 | `utils/file.py` `create_snapshot`/`restore_snapshot` | 删前 grep 确认无其他调用方 |
| SDK 配置透传 | `sdk/types/agent.py`、`sdk/builders/agent_config_builder.py`、`sdk/agent.py` | §6 |
---
## 3. WP1 — 摘除旧干扰源(先做)
### 3.1 Prompt 不再切换
- `operator.py` `_build_prompt`:删 `has_feedback` 分支,恒用 `main_template`。
- `CheckerFeedbackPromptComponent` 改写为 `CheckFeedbackPromptComponent`:读 state 的 `check_feedback`(§5.2 写入),无内容不渲染,加入恒定组件列表。
- 删 `_has_checker_feedback`、`_get_verification_chat_rounds`、`_get_reply_to_checker_tool` 及工具注册逻辑;`operator.json` `troubleshooter_template` 相关键清理(grep 确认无他处引用)。
### 3.2 每个 operator turn 都必须 record_step(含 planner 拒绝的 turn)
- 仅移除 `if checker_success:` 门控**不够**:planner 拒绝分支在 ~190 提前 `return`,同样跳过记录。
- 做法:把现有 record_step 代码抽成节点内局部函数 `_record_turn(extra_metadata: dict) -> str | None`:
- 正常路径原位调用(extra 不变);
- planner 拒绝分支在 `return` 前调用,extra 加 `{"planner_rejected": True}`(该 turn 的 terminal actions 被拦截未执行,记录如实反映决策与拦截)。
- 保留"planner 拒绝的 turn 不清零 subagent 循环计数"语义(`checker_success` 今后仅由 planner 拒绝路径置 False,维持现有 `if checker_success: update["subagent_calls"] = []` 写法)。
### 3.3 删除 notes 快照/回滚
- `operator.py` `create_snapshot` 段、`ctx.task_plan_snapshot` 赋值:删。
- `execution_check_node` "Handle optimistic execution rollback/commit" 整段(~252-280):删。
- `context.py` `task_plan_snapshot`:删。`utils/file.py` 快照函数 grep 无调用方后删。
- planner validation 拒绝时的 task_plan.md 单文件回滚(`ctx.task_plan_content_before`)**保留**。
### 3.4 删除单槽位与 chat 机制
- `execution_check_node`:删 chat_path 触发段与 `await ctx.checker_task` 消费段。
- `context.py`:删 `checker_task`、`checker_max_chat_rounds`。`planner_task` 保留。
- `_process_plan_write` `new_completions` 触发段:暂时整段删除(§5.1 重建)。
- `utils/verification.py`、`checker_status.json` 写入、相关 import:删。
WP1 完成后系统处于"无任何语义验证"的干净中间态,可独立运行与测试。
---
## 4. WP2 — 计划语法、Planner 与检查标准保护
### 4.1 plan grammar 扩展(`utils/plan_grammar.py`)
- 新语法:顶层子目标下缩进附属行,关键字带可选时机后缀:
```
- [ ] 在时钟应用创建 7:30 AM 闹钟
- verify: 闹钟列表应出现 7:30 AM 且开关为开
- assert: 创建后应弹出"已设定闹钟"提示
- [ ] …
(任务级,可置于计划末尾游离段)
- assert@end: 全程不应出现应用崩溃对话框
```
- 解析为:
```python
@dataclass(frozen=True)
class CheckItem:
kind: Literal["verify", "assert"]
when: Literal["on_complete", "at_end"] # 无后缀=on_complete;@end=at_end
text: str
parent_key: str | None # 挂靠子目标的 key;任务级为 None
```
`when` 语义:`on_complete` 在锚定子目标完成时刻用**当时证据**判定;`at_end` 只在出口用**最终状态**判定。**禁止**在出口用当前状态重判 `on_complete` 项,也禁止在中途提前执行 `at_end` 项。
- 新增 `CHECK_LINE_RE`;先确认 `parse_plan` 对非 checkbox 缩进行的容忍度(`CHECKBOX_LINE_RE` 只匹配 checkbox 行),确保 check 行**不影响** `milestones_changed`/`subgoal_hash`/`new_top_level_completions` 现有行为。
- `PlanSnapshot` 新增 `check_items_of(item)`、`task_level_check_items`、`all_check_items`;新增模块函数 `check_items_changed(before, after)`(比较 check 行多重集合)。
- **语法说明条件化**:`PLAN_GRAMMAR_SPEC` 从模块常量拆为"基础语法 + check 扩展"两段,新增 `render_plan_grammar_spec(include_checks: bool) -> str`。check 扩展段内容:两关键字语义(verify=完成度,失败触发修复;assert=测试断言,失败如实记录不修复)、`@end` 后缀、§1.2 能力边界声明(事后核验、不支持强制前置关卡、瞬态断言的证据约束)。三个现有注入点改用该函数:`planner.py` ~105/~178、`operator/prompts.py` ~206;`include_checks = not(两开关全关)`。全关时任何 prompt 里都不出现 check 语法。
### 4.2 Planner 生成检查项(`planner.json`)
- **生成指令是 planner prompt 的条件段**:检查项相关的全部指令(下述各条 + §4.1 的 check 语法扩展)只在 `include_checks` 为真时注入。两开关全关 → Planner 的 prompt 与产出和无此功能时完全一致,不生成任何 check 行。
- 用户目标里的显式预期 → `- assert:` 行,逐字保留用户语义;属于全程/最终状态的预期用 `@end`。
- `- verify:` 行**只为确需验收的关键节点**生成——判断标准:不可逆操作完成点、对最终目标有直接贡献的里程碑。**不是**每个子目标一条;克制是明确要求(成本与 check 项数量成正比,见 §6)。
- 不可验证的子目标不硬造断言;瞬态预期优先改写为可持久探测的状态断言。
- 用户提出"必须先确认 A 才能做 B"类强序要求时:断言照常生成,但按 §1.2 标注"事后核验",不得静默承诺前置拦截。
### 4.3 检查标准保护(确定性守护,不依赖 LLM)
- **前提事实**:planner validation 默认关闭(`disable_planner_validation=True`),不能作为 check 行不可删的唯一保障。
- **确定性守护**(`_process_plan_write` 内,Operator 来源的写入):写入后若 `check_items_changed(before, after)` 检出 check 行被删除或改写,Graph **把缺失的 check 行合并回写**进新计划(锚定其 `parent_key`;父子目标整个被删时,该行转为任务级 `@end` 项)。合并回写是纯文本操作、同步完成,不打断 Operator;Operator 新增 check 行允许保留。Planner 节点自身的计划写入不受此守护(Planner 有权修订检查标准)。
- planner validation **开启时**:check 行差异同样纳入其审计范围(prompt 增加"检查标准被弱化应拒绝"要点)。机制本体(CAS 回滚、ratchet baseline)不动。
### 4.4 Operator 可选说明段(`operator/prompts.py`)
- 新增 `CheckItemsExplainerPromptComponent`,加入 `main_template` 恒定组件列表,**仅当当前 task_plan.md 解析出 check 行时渲染**(以计划实际内容为准,不看开关——续跑的旧计划带 check 行时同样需要解释),否则输出空串。
- 内容(一段,短):
- `- verify:` 是该子目标的验收标准,执行时可以参考它确认做到位,但**判定由独立 Checker 执行**,不要自行宣布通过或替 checker 记录结论;
- `- assert:` 是测试断言,**不需要为它做额外动作**,更不得为满足断言去构造/伪造状态——断言失败是合法的测试结果;
- check 行**不可删除或改写**(删除会被系统自动恢复);重写计划时原样保留,新增允许;
- 正常按语法打完成标记即可,检查在后台异步进行,不阻塞执行。
- 这与 §4.1 注入 operator 的语法扩展分工:语法扩展讲"这些行是合法语法",本组件讲"你该怎么对待它们";前者受开关控制,后者受计划内容控制。
### 5.1 触发:排队与 spawn 分离(证据锚点修正)
- **`_process_plan_write` 只排队**:`new_top_level_completions` 检出新完成项后,对每个带 `on_complete` check 项的完成项,追加 `ctx.pending_checkpoints: list[PendingCheckpoint]`(字段:`checkpoint_id=subgoal_hash`、check 项、完成时计划全文、触发时间戳)。**不在此处 spawn**——此刻本 turn 的 step 尚未记录,`get_step_number` 读到的是上一轮,锚点会错位。
- **`execution_check_node` 在 record_step 之后 spawn**:对每个 pending checkpoint 生成 `attempt_id`(`{checkpoint_id}#{递增序号}`),构造证据锚:
```python
@dataclass(frozen=True)
class EvidenceAnchor:
anchor_step_id: str # 刚记录的本 turn step(含 pre 截图;上一 turn 的 post 截图已在库)
trigger_ts: float
plan_text: str # 完成时刻的计划全文
```
然后 `ctx.checkpoint_tasks[checkpoint_id] = (attempt_id, asyncio.create_task(run_checkpoint_check(...)))`。
- **supersede 不丢裁决**:spawn 前若同 `checkpoint_id` 已有旧条目——旧任务 `done()` → **先收割入账**(§5.2)再替换;仍在运行 → `cancel()` 并 `await`(吞 `CancelledError`,完成资源清理),账本记一条 `superseded` 记录(attempt_id、时间),再 spawn 新 attempt。
- 每个新完成项都触发(修复旧"只验 `new_completions[-1]`"bug)。中途开关关闭时不排队不 spawn(相关项出口按 §5.3 处理)。`user_stop_requested` 后不再 spawn 任何新检查。
- **并发上限**:`max_concurrent_checkpoints`(默认 3),超出的 pending 留在队列,下轮 execution_check 再 spawn。
### 5.2 收割(`execution_check_node` 内,非阻塞)
- 每轮遍历 `ctx.checkpoint_tasks`,**只收割 `task.done()` 的**(绝不 await 未完成的),收割后从 dict 移除:
1. **入账**:所有裁决 append-only 写入 DataEngine 账本(session 元数据或专用 jsonl:attempt_id、checkpoint_id、item_text、kind、when、status、evidence、anchor_step_id、时间)。这是"首次失败永久保留"的唯一载体,任何后续结果只能追加。
2. **状态副作用的适用性校验**:仅当该 attempt 仍是此 checkpoint 的**当前 attempt**、且锚定子目标在当前计划中文本未变时,才允许改执行状态;过期/失配的裁决只入账不作用。
3. `verify` FAIL(适用时)→ 子目标 `[x]`→`[/]`(`subgoal_hash` 匹配,直接写文件不走 wrapped note tool)+ finding 追加进 state `check_feedback`;**每 checkpoint 修复配额 `checkpoint_max_repairs`(默认 2)**:超配额不再打回,裁决保持 failed,留待出口如实呈现。`user_stop_requested` 后不再打回。
4. `assert` FAIL → 只入账,不打回不注入(可配 `assert_failure_policy: "continue" | "halt"`,halt=置停止标记走 §5.3 收尾)。
5. cancelled 记 `superseded`;异常/超时:verify 记 `inconclusive` + warning(**裁决不改写为 passed**,仅放行决策按通过处理),assert 记 `inconclusive`。
- **单次检查超时**:`checkpoint_timeout`(默认 180s,spawn 时用 `asyncio.wait_for` 包裹)→ 超时按异常处理。
- 副作用只发生在收割点与出口结算,任务体内零副作用。
### 5.3 出口:结算与终审分离
- `convergence_gate` 扩为 `Literal["continue", "exit_settlement", "end"]`:
- `ctx.data_engine` 为 None → `"end"`;
- 任务到达终态(`all_top_level_done`,或 continuous+user_stop,或 halt 标记)→ 只要**存在任何 check 项/未决任务/账本记录,或结尾终审开启**,一律 `"exit_settlement"`;两者皆无才 `"end"`。
- **空计划(`not has_top_level`)且结尾终审开启 → 仍走 `"exit_settlement"`**:没有计划项不等于无可验之物,终审对象是用户原始目标。
- 新增节点 `exit_settlement_node`,两阶段,**阶段一无条件执行,阶段二受结尾开关控制**:
1. **结算(barrier)**:收齐 `checkpoint_tasks` 全部未决任务并按 §5.2 入账——这是全流程唯一有意的阻塞等待,带**加总超时** `settlement_timeout`(默认 120s):超时 cancel 剩余任务、对应项记 `unchecked(timeout)`。已启动的检查必须在退出前有账,不论结尾开关。
2. **终审(`disable_final_check=False` 时)**:运行 `run_final_check`,对照 **`state.initial_goal`(用户原始目标,不是计划)** + 全部 check 项:
- `at_end` 项:用最终状态判定;
- `on_complete` 项:**引用账本裁决**(当前 attempt 的 PASS/FAIL 直接采信,FAIL 过的 assert 保持 FAIL);从未检查过的(中途关闭/配额限制),基于锚点附近的**历史证据**补判,证据不足记 `unchecked`——禁止用当前屏幕重判过程断言;
- 终审自身超时/异常:fail-open 放行,账记 `inconclusive`。
3. 路由:终审通过或关闭 → `END`;verify 类未达成(assert 失败**不算**)→ 打回 unmet 项 + `check_feedback` 注入 → `perception`;`final_check_attempts` 达上限(默认 3)或 user_stop/halt → `END`。
- 收尾状态见 §5.5;continuous+user_stop 时 FAIL 不回循环。
### 5.4 Checker 本体(`agents/checker/checker.py` 重写)
- 两个入口共用旧 `run_async_check` 的工具循环骨架(多轮循环、迭代上限 `checker_max_iterations`、结构化兜底,可整体沿用):
- `run_checkpoint_check(check_items, anchor: EvidenceAnchor, goal, ...)`
- `run_final_check(goal, plan_text, ledger, ...)`
- **输入**:用户原始目标、计划、待验项、证据锚、`get_agent_friendly_steps()` 摘要历史。
- **工具表按入口 × 配置装配(§1.3;工具表是唯一权限来源,全部只读)**:
| 工具 | `run_checkpoint_check` | `run_final_check` | 配置开关 |
|---|---|---|---|
| `get_step_detail(step_no)` / `get_step_screenshot(step_no, which: pre\|post)`(`storage.get_steps` + step 目录,新增只读辅助) | ✓ | ✓ | 恒定 |
| `read_note`(只读) | ✓ | ✓ | 恒定 |
| `probe_device(kind, params)`(固定探测类型 + 程序生成 argv,见下) | ✓ | ✓ | `disable_device_probes`(默认 False);关闭则**不注册且 prompt 零提及** |
| 实时屏幕读取(`UnifiedMobileController.get_screen_data()` + OCR,旧 checker 有现成代码) | **✗ 不装配** | ✓ | 恒定(仅 final 入口) |
中途检查点**不装配实时屏幕**是刻意的证据纪律:检查点的证据是锚点历史与持久状态探测,当前屏幕早已随 Operator 前进,装配它只会诱导用错证据——纪律靠工具表落实,不靠 prompt 劝告。
**任何入口都不注册**:设备 action、write_note、子代理。
- **`checker.json` 拆为组件段,按待验内容与入口装配**(不在单一模板里堆条件文本):
| prompt 段 | 装配条件 |
|---|---|
| 基础裁决规则(逐字引用、证据具体化、inconclusive 语义) | 恒定 |
| verify 语义段(完成度判定、fail-open 放行规则、suggestion 要求) | 待验项含 verify |
| assert 语义段(测试断言、失败即结果、证据缺失记 inconclusive 不记 failed) | 待验项含 assert |
| 锚点取证指南(瞬态/顺序断言必须查锚点附近历史) | `run_checkpoint_check` 入口 |
| 终审指南(对照原始目标、引用账本、at_end 用最终状态、禁止重判过程断言) | `run_final_check` 入口 |
| probe 使用指南(持久状态优先探测、探测类型速查) | `probe_device` 已注册 |
- **`probe_device` 设计(不做前缀白名单)**:`dumpsys` 等含状态修改子命令(如 `dumpsys battery set level 100` 可改电量模拟),且前缀判断挡不住参数拼接。改为枚举探测表,argv 列表由代码构造、不经 shell 字符串拼接,参数正则校验(拒绝空白符与 shell 元字符):
```python
PROBES: dict[str, ProbeSpec] = {
"alarms": argv=["dumpsys", "alarm"], # 无参数
"battery": argv=["dumpsys", "battery"], # 无参数(禁 set/unplug 等子命令)
"foreground": argv=["dumpsys", "activity", "activities"],
"notifications":argv=["dumpsys", "notification"],
"setting": argv=["settings", "get", <ns>, <key>], # ns∈{system,secure,global};key: ^[A-Za-z0-9._-]+$
"content": argv=["content", "query", "--uri", <uri>], # uri: ^content://[A-Za-z0-9./_-]+$
"packages": argv=["pm", "list", "packages"],
"prop": argv=["getprop", <key>], # key: ^[A-Za-z0-9._-]+$
}
```
未列举的探测类型一律拒绝;扩表需改代码,不接受运行时自由命令。底层复用 `run_adb_command` 的执行通道但以 argv 形式传递。
- **输出**(structured output):
```python
class CheckVerdict(BaseModel):
item_text: str # check 行原文,逐字
kind: Literal["verify", "assert"]
status: Literal["passed", "failed", "inconclusive"]
evidence: str # 具体观察,禁止空泛
suggestion: str # 仅 verify failed 需要
class CheckReport(BaseModel):
verdicts: list[CheckVerdict]
unmet_subgoals: list[str] # 仅 final check:逐字引用计划项
```
放行决策(passed 布尔)由 **node 侧**从 verdicts 计算(verify 全 passed/inconclusive → 放行),不由模型直接输出——保证"放行≠裁决改值"。
- **各段共同要点**:failed 必须附具体证据(探测输出或截图元素),空泛否决 node 侧降级 inconclusive;证据从未被采集时输出 inconclusive 并说明"证据缺失"而非 failed;引用计划项逐字。
### 5.5 任务收尾与 SDK 传导(放行 / 测试结果 / 收尾状态三轴分离)
- Graph 在 END 前汇总 **run outcome**(写入 state 新字段 `run_outcome` 与 DataEngine session 元数据):
```python
class RunOutcome(BaseModel):
task_status: Literal["completed", "partial", "blocked"] # 目标达成轴
tests: TestSummary # passed/failed/inconclusive/unchecked 计数 + failed 明细
```
- verify 未达成且预算耗尽 → `blocked`(或 `partial`);
- **assert 有任何 FAIL → `tests.failed > 0`,即使 `task_status="completed"`**——任务做完与测试通过是两回事。
- **SDK 链路必改**(`sdk/agent.py`):
- 图完成处(~718-721):不再无条件打 success 日志 / `end_session("completed")`——读 `run_outcome`:`blocked` → `end_session("failed")`(确认 `data_engine.end_session` 支持的状态集,必要时扩);`completed` 但 `tests.failed>0` → `end_session("completed")` 且 session 元数据带 test summary,日志明确打出"任务完成、N 条断言失败";
- task.status 收敛处(~1035-1040)与 trace 命名处(~1061 `_PASS/_FAIL`):断言失败的运行 trace 命名用 `_TESTFAIL`(或并入 `_FAIL`,二选一但必须与 `_PASS` 区分);
- `mobile_run_task`/`mobile_manage_task` 返回结构透出 test summary,调用方无需翻报告正文即可拿到机器可读结果。
- Outputter 报告呈现完整裁决序列与三态统计;BLOCKED/partial 收尾时最后一次 finding 入元数据。
---
## 6. WP5 — 配置与 SDK 透传
- `ExecutionSetup`(`context.py`):
- `disable_midway_checks: bool = False`、`disable_final_check: bool = False`(默认开。成本与计划中 check 项数量成正比——§4.2 已要求 Planner 只在关键节点生成 verify,这是成本上界的来源,**不存在"零成本"**);
- `disable_checker` 保留为兼容别名:True 时两者同关;
- `final_check_max_attempts: int = 3`、`checkpoint_max_repairs: int = 2`、`max_concurrent_checkpoints: int = 3`、`checkpoint_timeout: float = 180.0`、`settlement_timeout: float = 120.0`、`assert_failure_policy: Literal["continue","halt"] = "continue"`、`disable_device_probes: bool = False`;
- `disable_planner_validation`、`checker_max_iterations` 语义不变。
- SDK 透传链:`sdk/types/agent.py`、`sdk/builders/agent_config_builder.py`(`with_disable_checker` 保留 + 新开关映射)、`sdk/agent.py`。
- 四组合行为:
| 中途 | 结尾终审 | 行为 |
|---|---|---|
| 关 | 关 | 不 spawn、不终审;**已启动的任务仍出口结算**(本组合下无任务);全部 check 项报 unchecked |
| 关 | 开 | 出口结算(空)+ 终审:`at_end` 判最终状态,`on_complete` 凭历史证据补判,不足记 unchecked |
| 开 | 关 | 检查点照跑;出口**只结算不终审**(阶段一无条件),账本与 test summary 照常产出 |
| 开 | 开 | 检查点 + 结算 + 终审 |
---
## 7. 明确不做(Out of scope,勿顺手扩大)
- **强制前置关卡**("确认 A 后才允许执行 B"的中途阻塞等待)——本期只提供事后核验;出口 barrier 保证的是**退出前完成裁决结算**,不保证执行顺序约束。此边界必须如实声明(§1.2),不得静默接受此类需求。
- "动作前"检查时机(前置状态是 Operator 感知职责)。
- 逐动作依赖分析。
- 多验证器投票、版本化 VerificationKey、shadow 遥测。
- Validator 拆分、统一 FailureEvent/RecoveryCoordinator、`_check_infinite_loop` 降级。
---
## 8. 测试
现有测试(先跑通再动手,改完逐个修):
- `tests/unit/graph/test_graph.py` —— execution_check_node / convergence_gate 断言大改。
- `tests/unit/agents/test_plan_mutation_guardrails.py` —— planner validation 主体不动;补 check 行确定性守护用例(§4.3)。
- `tests/unit/utils/test_plan_grammar.py` —— verify/assert/@end 解析、不影响 milestone hash。
- 全仓 grep `verification_chat`、`checker_status`、`task_plan_snapshot`、`create_snapshot`、`troubleshooter`、`ctx.checker_task`、`checker_max_chat_rounds`:零残留。
新增(建议 `tests/unit/graph/test_checkpoints.py` + `test_exit_settlement.py`):
1. 排队/spawn 分离:`_process_plan_write` 只排队;execution_check 在 record_step 后 spawn,anchor_step_id 等于本 turn 刚记录的 step。
2. 多个同轮完成项全部排队;无 check 项不排队;中途开关关不排队;user_stop 后不 spawn。
3. supersede:旧任务 done 未收割 → 先入账再替换(**失败裁决不丢**);运行中 → cancel 且 await 完成,账本有 superseded 记录。
4. 收割只取 done;适用性校验:过期 attempt / 子目标文本已变 → 只入账不打回。
5. verify FAIL 打回 + 注入;超 `checkpoint_max_repairs` 后不再打回、裁决保持 failed。
6. assert FAIL 只入账;`halt` 策略触发收尾;账本 append-only(先 FAIL 后 PASS 两条都在)。
7. 超时:单任务超 `checkpoint_timeout` → inconclusive;结算超 `settlement_timeout` → 剩余 cancel + unchecked(timeout)。
8. 出口路由:结算与终审解耦——终审关、有未决任务 → 仍进 exit_settlement 且只走阶段一;空计划 + 终审开 → 进终审;全关全无 → 直接 end。
9. `when` 语义:`at_end` 不在中途执行;`on_complete` 在终审引用账本、无账本凭历史补判、证据不足 unchecked(构造"当前状态已满足但历史证据缺失"的用例,断言**不**判 passed)。
10. record_step 无条件:正常 turn 与 planner 拒绝 turn 都有记录,后者带 `planner_rejected` 标记。
11. check 行守护:Operator 写入删除 check 行 → 合并回写;父目标被删 → 转任务级 `@end`;Planner 写入可修订;planner validation 关闭时守护独立生效。
12. `probe_device`:枚举表内放行、表外拒绝、参数带空白/元字符拒绝、`battery set` 类子命令无法表达。
13. fail-open 三轴分离:verify 异常 → 放行但账记 inconclusive(**不是 passed**)。
14. run outcome / SDK:assert 失败 → `tests.failed>0`、trace 名非 `_PASS`、`end_session` 元数据含 summary;verify 预算耗尽 → `blocked` + `end_session("failed")`。
15. continuous+user_stop+FAIL → 不回循环。
16. 四开关组合矩阵(§6 表逐行)。
17. prompt 条件化:两开关全关 → `render_plan_grammar_spec` 输出不含 check 语法、planner prompt 无生成指令;`CheckItemsExplainerPromptComponent` 仅在计划含 check 行时渲染(含"开关关但旧计划带 check 行仍渲染"的用例)。
18. checker 装配:`run_checkpoint_check` 的工具表**无实时屏幕**、`run_final_check` 有;`disable_device_probes=True` → `probe_device` 未注册且 prompt 无探测指南;待验项全 verify → prompt 无 assert 段(反之亦然);checkpoint 入口无终审指南段。
运行:`python -m pytest tests/unit/graph tests/unit/agents tests/unit/utils -x`(需要假 LLM 设 `ARTEMIS_FAKE_LLM=1`)。
---
## 9. 验收标准
1. Operator 的 prompt 模板、notes、DataEngine 历史在任何验证结果下不被回滚/切换/丢弃;**每个 operator turn(含 planner 拒绝的)都有 step 记录**;中途路径上不存在对未完成验证任务的等待。
2. verification chat、`checker_status.json`、notes 全量快照、单槽 `ctx.checker_task` 从代码库消失。
3. 检查项由 Planner 声明(verify/assert × on_complete/at_end),Operator 删除/弱化 check 行会被**确定性守护**恢复,不依赖 planner validation 开关。
3a. **§1.3 装配纪律全局成立**:检查全关时 planner/operator prompt 与无此功能时一致(零污染);计划含 check 行时 Operator 收到行为说明段;checker 的 prompt 段与工具表按入口 × 配置装配(checkpoint 入口无实时屏幕、probe 关闭则工具不注册且 prompt 零提及、无 assert 项则无 assert 段);不存在指向已关闭机制的死指令。
4. 检查点在 record_step 之后 spawn,证据锚指向正确的 step;裁决全部 append-only 入账,supersede 不丢已完成裁决;检查有单次超时、并发上限、修复配额。
5. 出口结算无条件执行(含加总超时与 unchecked 标记),终审受结尾开关控制;空计划不绕过已开启的终审;终审对照用户原始目标;`at_end`/`on_complete` 判定时机不错位。
6. 放行、裁决、收尾三轴分离:fail-open 不改写裁决值;assert 失败产生机器可读 test summary 并传导至 `sdk/agent.py` 收尾(日志、end_session、trace 命名、返回结构)。
7. `probe_device` 为枚举探测 + 程序生成 argv,无自由命令通道。
8. 计划/报告如实声明"事后核验"边界,强序需求不被静默接受。
9. planner validation(入口卡点)除新增 check 行审计外与现状一致。
10. §8 测试全绿。
---
## 10. 建议执行顺序
WP1(3.2 → 3.3 → 3.1 → 3.4,摘干扰源,得到干净中间态)→ WP2(§4 语法 + Planner + 守护,独立可测)→ §5.4 checker 本体与 probe 表(可单测)→ §5.1 触发 → §5.2 收割 → §5.3 出口 → §5.5 收尾传导 → WP5 配置透传 → 全量测试。
每步完成后系统都应可运行:WP1 后=无验证;WP2 后=计划里有 check 项但无人消费(守护已生效,无害);§5.4 后=checker 可单测;接线后=全功能。
-598
View File
@@ -1,598 +0,0 @@
# ARTEMIS 上下文与记忆重构:显式状态、按 agent 可见域、统一记忆内核
状态:提案 v2(2026-08-31,经"不损伤"审计细化,待终审)
> **2026-08-31 更新**:Phase 0/1 已实施;Phase 4(§6 方案 D 记忆内核)由
> `docs/plans/history-module-redesign.md`(统一方案 v3)取代并修订——其中
> "两 lens 按 profile 分立"修订为"步级视觉 lens + 段级胶囊 lens 按高度分立"
> (该修订已于 2026-08-31 获用户拍板采纳),
> StepMemoryService 运行时与 ContextPolicy 策略表保留。Phase 2/3 不受影响。
>
> **最终状态(2026-09-01)**:统一方案 v3 已全部实施(M0-M5,worktree
> `zealous-gates-4626b1`,未提交)——StepMemoryService/ContextPolicy 均已按
> 该方案落地;附录 B 中"保留、移除权属姊妹文档 §3.3"的 `short_term_memory`
> 台账行已在 M5 核销移除(全部消费者先核销后删,台账纪律照本文档执行)。
> 各里程碑实施标注见 history-module-redesign.md §7。
范围:State / ArtemisContext 的类型化与生命周期分域;感知数据通道;Flash 压缩机制上提为通用记忆内核。
底线与目标:**不损伤为最低要求**——每一处删除/搬迁都在附录 B 台账中核销其全部消费者,
任何信息流不得静默中断;**优雅处理所有信息为目标**——每份数据有唯一属主、显式通道、
声明的可见域,重复实现收敛为单一入口。
姊妹文档:`docs/design/pro-context-memory-redesign.md`(2026-08-25)深度覆盖了 Pro 侧
"压缩什么、怎么压"(视觉摘要、历史分块、召回工具、token 预算)。本提案覆盖它没有处理的
另一半——**数据放在哪、谁能看见什么、通道是否显式**——并在 §6 把两份文档的交叠处收敛为
同一个记忆运行时。
---
## 1. 调查结论:现状与证据
### 1.1 实际存在四个上下文平面,只有一个有主人
| 平面 | 载体 | 生命周期 | 现状 |
|---|---|---|---|
| P1 图通道 | `State`(pydantic + `extra="allow"` + reducer) | 跨节点、跨回合 | 声明字段与实际使用严重脱节(见 1.2) |
| P2 就地突变 | 同一个 State 实例被节点/工具直接赋值 | **节点内**(经 `InjectedState` 同实例) | 无声明的隐式契约;经核验其全部消费都是节点内的(见 5.3) |
| P3 ctx 动态属性 | `ArtemisContext`(`extra="allow"`) | 进程/运行期 | `latest_perception_data` 等动态挂载,无 schema |
| P4 持久存储 | DataEngine(步骤/截图/摘要)+ notes 文件 | 持久 | **唯一健康的平面**:单一写入路径,多消费者 |
Pro 的真实架构其实很好:Operator 每回合从 P4 **重新编译**全量 prompt
(`operator.py:624` → `build_plan_and_history`),不靠累积消息。问题不在架构方向,
而在 P1–P3 三个平面无 schema、无属主、互相兜圈子。
### 1.2 `extra="allow"` 已经吞掉的真 bug(本次调查实证)
**B1 幽灵键 `ui_tree`:4 个读取点,0 个写入点。**
`operator.py:995`、`validator/tool_declarations.py:477`、`mcp/action_executor.py:401`、
`tools/mobile/exec_tools.py:548` 均 `getattr(state, "ui_tree", None)`,但全仓无人写入
(真实数据在 `latest_ui_hierarchy`)。后果:`utils/coordinates.py:185-215` 的
"从层级树找主滚动容器"分支**永远走不到**,smart swipe 静默降级。
**B2 异步 Planner 驳回反馈被静默丢弃。**
`graph.py:195` 把驳回理由写进 `validator_messages` 通道,但该通道**没有任何节点读取**
(`operator.py:388` 和 `tool_wrapper.py:175` 只是当场拆 `Command.update` 信封,从不读
State 通道)。后果:计划被回滚了,Operator 下一回合看到回滚后的计划,却永远看不到
驳回理由。同时 `validator_messages` 经 `add_messages` 单调累积整轮所有 ToolMessage,
无人消费、无人裁剪。
**B3 `ctx.latest_perception_data` 孤儿写。**
`perception.py:187` 每轮把整张截图 b64 挂上 ctx,注释声称"给 Checker 零延迟复用";
但工作区的 checker 重写已改为自己抓屏(`checker.py:620-652`)+ 从 DataEngine 取历史
证据。全仓**零读取点**,纯内存驻留。
**B4 `indexed_elements` 只写实例不回通道。**
`operator.py:649-650` 就地写两个字段,但 `operator.py:766` 的 update 只回传
`indexed_points`,漏了 `indexed_elements`。审计确认跨节点消费实际不存在(validator 侧
自行重推导,`action_executor.py:173-174`),但两键不对称是持续的误导源。
**B5 更多幽灵/半死键与坏路径。** 全表见附录 B。要点:
约 9 个只初始化/只读不写的死键;`graph_runner.py:48` 的
`getattr(self.ctx, "artemis_context", None)` 恒 None → 构造出丢失 data_engine 的空
ArtemisContext;且 `reactive_runner.py` 的 `State(messages=[])` 缺必填字段构造即抛
——engine 双 runner 的真机路径今日均必炸;`_active_driver` 未声明为 `PrivateAttr`。
(v2.1 更正:初版审计将 `operator_agent.py:63` 的 `task_goal` 与
`failure_analyzer_agent.py:48-50` 的 `failure_context` 列为图 State 幽灵键,实为
engine/BaseAgent 栈自己的 state 协议——`ExecutionContextState.task_goal` 存在
(core/state.py:48),`.get()` 有 `isinstance(state, dict)` 防护。两处均无需修改。)
**B6 感知数据同屏最多 4 份拷贝、抓屏三处重复实现。**
同一帧 b64 同时在 `ctx.latest_perception_data`(死)、`state.operator_raw_data`
(走图通道且随 `astream` values 流每超步复制,`sdk/agent.py:694-696`)、operator 局部、
prompt data-URL。抓屏管线在 perception_node(settle+OCR+融合)、validator 动作后轮询
(`validator.py:117-140`)、checker 终检(`checker.py:620-652`)三处各写一遍,
planner 首帧还有第四处自拍(`planner.py:211-236`,注意 planner 在图中先于 perception
运行,这是常规路径不是边角)。
### 1.3 Flash 与 Pro 的压缩机制现状
| | Flash | Pro |
|---|---|---|
| 上下文模型 | 累积消息 + 每轮幂等原地压缩(`compress_flash_messages`) | 每回合从 DataEngine 重编译(`build_plan_and_history`) |
| 步骤摘要 | `VisualStepSummarizer`:前后截图 + 动作红标叠加,客观中立措辞契约,异步无阻塞,无界重试 | `SummarizerNode`:**纯文本**(不加载 pre/post 图),3 次退避重试,无 flush 收尾语义 |
| 摘要落点 | `data_engine.update_step_summary`(同一个 API!) | `data_engine.update_step_summary` |
| 未就绪降级 | 摘要 pending → 保留原图(lossless pending) | 无 summary → detailed 渲染(同构原则的另一形态,`task_tree.py:917-921`) |
| 历史可见性策略 | 无(30 轮上限内全量) | 三种互斥策略 + 6 个调用点各自硬编码 kwargs |
关键事实:**两条路径的摘要已经写进同一个 DataEngine 字段**。但两者的摘要语义结构性
不同(见 6.1),统一的只能是运行时,不是语义。
---
## 2. 设计原则
1. **上下文是编译出来的,不是累积出来的。** Pro 已经如此;把它升格为正式模型,
Flash 的消息列表只是同一份记忆的另一种渲染。
2. **一份情景记忆,多种视图。** DataEngine 是唯一事实源;每个 agent 看到的是按其
角色声明的"最高价值密度视图"。agent 的智能上限由输入质量决定。
3. **可见域靠声明与类型约束。** `extra="forbid"` 拦截幽灵**写**与非法构造;幽灵**读**
(`getattr` 带默认值不会因 forbid 报错)由节点 READS/WRITES manifest 的测试期
代理拦截。两者缺一不可。
4. **重物不过通道。** 截图 b64 不进 LangGraph 通道;通道里只放引用,重物放进有属主、
有生命周期的 store。
5. **就地突变不消灭,而是正名。** P2 平面的全部用途经审计都是"节点内工具通信"——
给它一个声明的、节点局部的载体(TurnWorkspace),而不是假装它不存在。
6. **后台增强不可成为依赖。**(继承姊妹文档 §4.6)摘要未就绪必有无损降级。
7. **不损伤:删除必先核销。** 每个字段/属性/通道的处置都对照附录 B 台账,列明全部
消费者与同步改动点;无消费者证据不删。
---
## 3. 方案 A:State 显式化与按 agent 可见域
### 3.1 State 收敛为四组显式字段,`extra="forbid"`
```python
class State(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
# ── 控制面(跨回合、路由与信号)──────────────────────
initial_goal: str
injected_instruction: str | None # take_last
user_stop_requested: bool # sticky_or
checker_success: bool | None # take_last
operator_feedback: list[str] | None # take_last;见 3.3
exit_settlement_route: str | None # take_last
run_outcome: dict | None # take_last
# ── 回合产物(上一回合的决策与结果)───────────────────
current_step_id: str | None
structured_decisions: str | None
operator_raw_thinking: str | None
operator_native_thinking: str | None
short_term_memory: str | None # 活跃字段;移除权属于姊妹文档 §3.3
last_execution_result: dict | None
subagent_calls: list[str]
operator_tool_limit_exceeded: bool | None
# ── 感知引用(重物不进通道,见方案 C)─────────────────
perception: PerceptionRef | None # take_last
# ── 节点局部工作区(声明但非通道语义,见 5.3)─────────
workspace: TurnWorkspace | None = None # 不进 update、不跨节点
```
**处置总表见附录 B。** 与 v1 的差别:`short_term_memory` 保留(它是活跃读写对,
operator.py:743-765 写 → prompts.py:331 读;删除属姊妹文档 Phase 1 的事,本方案
不越权);`indexed_*` 不再声称"并入冻结快照"(见 5.3);`messages` 删除需同步改
三处消费代码(`sdk/agent.py:1182-1187` 兜底输出、`sdk/agent.py:1321-1325` stderr
打印、`engine/graph_runner.py:57` 入参——三处今日均为空转/坏路径,删除无信息损失,
但必须同 PR 内改掉)。
`asanitize_update`(`state.py:152-159` 空壳直通)删除;其在 14 处工具里的调用
随信封迁移(3.3)一并消失。
### 3.2 可见域:节点声明读/写集,测试期强制
每个节点类声明清单,运行期零开销,测试期强制:
```python
class OperatorNode:
READS = frozenset({"initial_goal", "perception", "operator_feedback",
"short_term_memory", "subagent_calls", "current_step_id",
"operator_tool_limit_exceeded", "injected_instruction",
"structured_decisions"})
WRITES = frozenset({"structured_decisions", "operator_raw_thinking",
"operator_native_thinking", "short_term_memory",
"current_step_id", "subagent_calls",
"operator_tool_limit_exceeded"})
```
- 新增 `artemis/graph/visibility.py`:`StateView(state, reads)` 代理,未声明字段访问
即抛 `VisibilityError`;`check_update(update, writes)` 校验返回键。
- 仅在测试与 `ARTEMIS_STRICT_STATE=1` 下启用;生产路径直接传 State。
- 附录 B 的实证读写清单即初始 manifest;上线先按现状放宽,再逐步收紧。
- **幽灵读的防线在这里**,不在 `extra="forbid"`(forbid 管不住 `getattr(x, k, None)`)。
### 3.3 工具信封与反馈通道拆分(修 B2)
`validator_messages` 承担两个不相干职责,拆开:
**1)工具返回信封 → 显式返回类型。**
审计确认全部 14 个工具写入点(`exec_tools.py` 5 处、`scratchpad.py` 6 处、
`launch_app.py:191`、`ocr.py:185`、`video_tool.py:161`、`object_detector.py:210,227`)
的 `Command.update` **只含**这一个键(以 `exec_tools.py:215-221` 为样本核验),
迁移是纯机械的:工具直接返回 `ToolOutcome(content, status)`,
`tool_wrapper.get_tool_result_content`(`tool_wrapper.py:175-176`)与 operator 循环
(`operator.py:388-391`)的拆信封逻辑同步简化。`graph.py:526` 的计划驳回信封同理
(它经 `_process_plan_write` 返回给 operator 循环当场消费,属节点内信封)。
通道及 `add_messages` 无界累积随之消失。
**2)跨回合反馈 → 统一 `operator_feedback` 通道。**
`check_feedback` 更名并扩容为 `operator_feedback`:来源包括 checker harvest
(`graph.py:198,230`)、exit settlement 弹回(`graph.py:367`)、以及**新接入的**
异步 Planner 驳回(`graph.py:195` 改写到这里,条目带 `[planner]` 前缀,与现有
`[final check]`/`[verify failed]` 前缀惯例一致)。渲染组件更名
`FeedbackPromptComponent`,提示语从"来自独立 Checker"改为按条目前缀中性表述。
现有读者同步改名:operator prompt(`prompts.py:243`)与 diagnoser
(`diagnoser.py:178`)——diagnoser 因此也能看到 planner 驳回上下文,属增益。
---
## 4. 方案 B:ArtemisContext 分解为组合根
ctx 保留为唯一入口(避免全仓签名大改),字段按属主聚类成显式子对象,
`extra="forbid"`:
```python
class ArtemisContext(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
# 基础设施(启动后只读)
device: DeviceContext
llm_config: LLMConfig | None
model_router: Any | None
agent_config: Any
adb_client / ui_adb_client / actuator / action_session / mcp_*
# 运行服务(有生命周期、有属主)
execution_setup: ExecutionSetup | None
data_engine: DataEngine | None
perception: PerceptionStore # 方案 C,取代 latest_perception_data
step_memory: StepMemoryService | None # 方案 D,统一摘要运行时
background: BackgroundTaskGroup # 吸收 background_tasks/jobs + 宽限期
# 编排状态(从散字段聚成对象)
checkpoints: CheckpointLedger # 吸收 pending_checkpoints/checkpoint_tasks/
# attempt_seq/repairs/final_check_attempts/assert_halt
plan_governor: PlanGovernor # 吸收 task_plan_content_before/
# last_validated_plan/pending_validated_plan/planner_task
run_outcome: dict | None = None # 显式化(今日为 sdk/agent.py:721 动态挂载)
package_cache: dict[str, str | None]
_active_driver: Any | None = PrivateAttr(default=None)
```
`CheckpointLedger` 与 `PlanGovernor` 不是新逻辑,只是把 `graph/checkpoints.py` 和
`graph/graph.py::_process_plan_write` 已在操作的字段簇搬进属主类型。
`ExecutionSetup` 审计后可直接上 `forbid`(唯一运行期赋值 `app_lock_status` 为已声明
字段,`sdk/agent.py:997`)。`graph_runner.py:48` 的 `artemis_context` 恒 None 坏路径
在 Phase 0 修复(显式传入或显式报错,禁止静默构造空 ctx)。
---
## 5. 方案 C:感知走显式通道(PerceptionStore + TurnWorkspace)
### 5.1 数据模型
```python
class PerceptionSnapshot(BaseModel):
"""一帧感知的产物。frozen:每次捕获生成新快照,绝不就地改。"""
model_config = ConfigDict(frozen=True, extra="forbid")
step_id: str | None # 关联的 DataEngine step(轮询帧可为 None)
image_name: str # sha256,DataEngine 图片主键
screenshot_path: str | None
screenshot_b64: str # 重物:只存在于 store,不进图通道
width: int
height: int
captured_at: float
kind: Literal["decision", "post_action", "probe"]
# 完整管线产物(raw 帧为 None)
raw_xml: list[dict] | None
fused_elements: list[dict] | None # OCR+XML 融合
ocr_results: list | None
minimal_list: str | None # 派生物在此算一次,消费者不再各自重算
class PerceptionRef(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
step_id: str | None
image_name: str
width: int
height: int
class PerceptionStore:
"""ctx.perception。环形缓存最近 N=4 帧;更早的按 image_name/step_id 从
DataEngine 回捞重建(b64 懒加载)。"""
async def capture_full(self, *, settle: bool, kind: str) -> PerceptionSnapshot: ...
async def capture_raw(self, *, kind: str) -> PerceptionSnapshot: ...
def put(self, snap: PerceptionSnapshot) -> PerceptionRef: ...
def latest(self, *, full_only: bool = False) -> PerceptionSnapshot | None: ...
def get(self, ref: PerceptionRef | str) -> PerceptionSnapshot | None: ...
```
**抓屏统一收口**:现有三处重复管线 + planner 自拍全部改走 store——
| 现调用点 | 改为 | 备注 |
|---|---|---|
| `perception_node`(settle+OCR+融合,`perception.py:116-168`) | `capture_full(settle=..., kind="decision")` | settle 启发式与 OCR/融合逻辑原样搬入 store,行为不变 |
| validator 动作后轮询(`validator.py:117-140`) | 轮询保持轻量私有;**最终帧** `capture_raw(kind="post_action")` 入 store | 轮询中间帧不入 store,避免噪音;最终帧成为可引用证据(今日被就地写进 `state.latest_screenshot` 后即失名) |
| checker 终检自拍(`checker.py:620-652`) | `capture_full(kind="probe")` | 其 OCR+裁剪逻辑与 perception 重复,删除 |
| planner 首帧自拍(`planner.py:211-236`) | `capture_full(kind="decision")` | planner 先于 perception 运行是常规路径,必须显式支持 |
由此**每一帧都自动进入环形缓存与 DataEngine**,异步消费者(Checker 检查点)持
`PerceptionRef`/`image_name` 而非活对象——缓存被挤掉也能从 DataEngine 重建,
天然解决"异步任务引用过期感知"的悬挂问题。
### 5.2 通道改造
- `perception_node`:`capture_full` → `put()` → update 只含
`{"perception": ref, "injected_instruction": ..., "user_stop_requested": ...}`。
删除 `ctx.latest_perception_data`(B3)与 `operator_raw_data` 过通道(B6:
b64 不再随 `astream` values 每超步复制)。
- 消费者一律 `ctx.perception.get(state.perception)` 或 `latest()`:
- **Operator**:快照直取 `minimal_list`/`fused_elements`,不再自算;
- **Validator 内工具**(object_detection 兜底 `object_detection_tool.py:93` 今日读
validator 就地写的 `latest_screenshot`):改读 `latest()`——validator 每次动作后
`capture_raw` 入 store,时序语义与今日一致;
- **Diagnoser/Committee/Explorer/FA**:同一入口,删除各自的文件重读与 getattr 链;
- **Outputter**:今日读最终 State 的 `operator_raw_data`(最后决策帧)。改为
`latest()`(最后 post_action 帧,比决策帧更新,属增益);若需保持逐字节兼容,
改读 `get(final_state.perception)` 即决策帧——落地时二选一并记录;
- 幽灵键 `ui_tree` 的 4 个读取点改读 `snapshot.fused_elements`(B1 终态修复;
Phase 0 先直改 `latest_ui_hierarchy` 止血)。
### 5.3 TurnWorkspace:给 P2 平面正名(修 B4 的正确姿势)
审计结论:`indexed_points`/`indexed_elements` 是**回合内可变工作集**——
explorer 增量 append 候选(`explorer_tool.py:311-315`),validator/FA 在动作后
整体重推导(`action_executor.py:173-174`);且全部消费都发生在**产生它的节点内部**
(operator 在返回前就把索引翻译成坐标;validator 侧自给自足)。跨节点传递从未
被需要——B4 之所以没被发现,正因为如此。
因此它们**不属于冻结快照,也不属于图通道**,属于节点局部工作区:
```python
class TurnWorkspace(BaseModel):
"""节点局部的可变工作集。经 InjectedState 对同节点工具可见;
不进 update、不跨节点、每节点从快照重播种。"""
model_config = ConfigDict(extra="forbid")
indexed_points: list[list[int]] = []
indexed_elements: list[dict] = []
@classmethod
def seed_from(cls, snap: PerceptionSnapshot) -> "TurnWorkspace": ...
def reseed(self, snap: PerceptionSnapshot) -> None: ... # validator/FA 重观察
def extend_candidates(self, cands: list[dict]) -> None: ... # explorer 增量
```
- `state.workspace` 为声明字段但**永不出现在任何节点的 update 里**(visibility
manifest 强制),每个需要它的节点入口自行 `seed_from(store.get(state.perception))`。
- 隐式契约("恰好同一实例")就此变成显式契约("声明的节点局部容器 + 三个具名操作")。
- `state.indexed_points`/`indexed_elements`/就地写 `latest_screenshot`/`operator_raw_thinking`
等全部 P2 突变点随之收编(thinking 就地写是给工具内 `record_step` 读的,同为
节点内通信,归入 workspace 或显式传参,落地时按触点少者取舍)。
---
## 6. 方案 D:记忆内核统一(Flash 压缩上提)
### 6.1 一个运行时,两份摘要契约
两个摘要器的差异是**结构性的**,不能合并语义:
- Flash 的摘要是**证据替身**——消息流保留了模型全部推理原文,压缩器只摘图,
摘要因此必须是纯客观的视觉转换描述,掺入意图即是污染;
- Pro 的摘要是**整步胶囊**——`build_plan_and_history` 丢弃旧步骤的全部思考,
一行摘要+精确动作+Validator 结果就是该步的全部遗留,因此必须压缩意图/策略/
进度(这正是姊妹文档 §8.2 坚持 `summarizer.md` 为语义唯一权威的原因)。
因此统一的是**机制层**,分离的是**语义层**。新建
`artemis/memory/step_memory.py`:
- **共享运行时 `StepMemoryService`**(合并两边的调度骨架):
- 键:`step_id`(Flash 的 `tool_call_id` 保留为别名映射;`dispatch` 已有
`data_engine_step_id` 参数,接线即可);
- 调度:零阻塞 dispatch + **有界**重试 + 显式降级态(替换 Flash 的无界重试与
Pro 的 3 次退避;此为有意的行为变更,见 §8)+ `flush(timeout)` 收尾语义
(Pro 目前 run 结束时未完成摘要行为未定义,直接补上);
- 落点:`data_engine.update_step_summary`;状态机(pending/ready/failed)与
版本化按姊妹文档 §7.1;lossless-pending 原则由消费侧共同遵守;
- 素材来源:pre/post 图直接以 `PerceptionRef` 引用(方案 C 的红利:不再在
dispatch 参数里搬运 bytes,`summarizer.py:92-100` 的 `_step_inputs` 图字节
缓存与手动置 None 回收逻辑消失)。
- **两份摘要契约(lens),各 profile 只跑自己的**:
- `VisualTransitionLens`(Flash):输入 pre/post 截图(动作红标叠加)+ 动作 +
Controller 结果;产出客观视觉转换,`flash_summarizer.md` 禁判定词契约不变;
- `StepCapsuleLens`(Pro):输入按姊妹文档 §8.1(决策图 + 可选后图 + 动作 +
Validator 结果 + FA 记录 + 当步思考 + 少量近期摘要);产出意图/策略/进度
胶囊,`summarizer.md` 为语义权威,视觉仅作接地、仍禁自行判定成败
(姊妹文档 §4.5 事实优先级)。
- 单 profile 单 lens,`update_step_summary` 单字段无冲突;未来若 Pro 需同时保留
视觉转换记录,按姊妹文档 §7.1 版本化元数据加 lens 标记,不在本期范围。
- Pro 图中 `SummarizerNode` 退化为 `dispatch(StepCapsuleLens)` 一行调用;
Flash `runner.py` 同理换成 `dispatch(VisualTransitionLens)`。
### 6.2 两个渲染器,一张策略表
渲染器保持各自形态(合理的方言差异,不强行同构):
- `compress_flash_messages`:消息列表原地压缩,摘要后端换成共享运行时;
- `build_plan_and_history`:编译式渲染,无改动即消费到视觉接地摘要。
把 6 个调用点散落的硬编码 kwargs 收拢为**每 agent 一条声明**,新建
`artemis/memory/context_policy.py`:
```python
CONTEXT_POLICIES: dict[str, ContextPolicy] = {
"operator": ContextPolicy(strategy="strict_milestone", last_n_detailed=1, recent_window=3, chronological_last=True),
"planner": ContextPolicy(strategy="strict_milestone", last_n_detailed=2, recent_window=5, chronological_last=True),
"failure_analyzer": ContextPolicy(strategy="strict_milestone", last_n_detailed=1, recent_window=3, for_failure_analyzer=True),
"checker": ContextPolicy(strategy="milestone_whitelist"),
"diagnoser": ContextPolicy(strategy="milestone_whitelist", window_from_config="history_window_steps"),
"committee": ContextPolicy(strategy="milestone_whitelist", last_n_detailed=3),
"outputter": ContextPolicy(strategy="full", last_n_detailed=0),
"summarizer": ContextPolicy(strategy="sliding_window", last_n_detailed=1, min_summaries=10),
}
```
这张表与 3.2 的 READS/WRITES manifest 互为镜像:一个声明"看 State 的哪些字段",
一个声明"看历史的哪个投影"。合起来就是"按 agent 划分可见域"的完整答案,
也是今后调优各 agent 输入质量的唯一改动点。收拢时**逐调用点对拍**:策略表渲染
输出与现 kwargs 输出必须一致(对拍测试进 CI)。
### 6.3 与姊妹文档的配置收敛
姊妹文档 §14 主张 Pro 单独 `agent.pro.memory`、不复用 Flash `step_summarizer`。
本方案在语义层同意(lens 分离),仅在**运行时层**共享:
```jsonc
"agent": {
"memory": { // 共享运行时:重试/flush/并发 + 策略表覆盖
"runtime": { "max_concurrency": 1, "retry_limit": 3, "flush_timeout_s": 30 },
"policies": { "operator": { "recent_window": 3 } }
},
"flash": { "step_summarizer": { /* VisualTransitionLens:模型、开关(现有键保持兼容) */ } },
"pro": { "memory": { /* StepCapsuleLens 模型/输入项 + token 预算等,per 姊妹文档 §14 */ } }
}
```
姊妹文档的 L3 历史分块、`recall_history`、token 预算不变,全部建立在同一个
`StepMemoryService` 产物之上。
---
## 7. 分阶段落地(已按依赖重排,消除 v1 的顺序隐患)
**Phase 0 — 修 bug(不动架构,可独立合入,零行为风险)** ✅ 已实施 2026-08-31
1. B1 止血:4 处 `ui_tree` → `latest_ui_hierarchy`(终态在 Phase 2 改快照)。✅
2. B2:`graph.py` 驳回反馈改走现有 `check_feedback` 通道(条目带 `[planner]`
前缀,弃写无人读的 validator_messages),`CheckFeedbackPromptComponent`
提示语按前缀中性化(标题保持 "Verification Findings" 不变,兼容既有断言)。✅
3. B3:删 `ctx.latest_perception_data` 写入与过期注释。✅
4. B4 止血:operator update 补回 `indexed_elements`(消除两键不对称;终态在
Phase 2 收编进 workspace)。✅
5. B5:`_active_driver` → `PrivateAttr`;新增 `State.initial()` 工厂统一初始状态
构造(sdk `_get_graph_state` 与 engine 双 runner 三处收拢单源);
`ExecutionContext.artemis_context` 显式声明;`graph_runner`/`reactive_runner`
真机路径缺 ctx 时显式 RuntimeError(替代静默构造空 ctx 后期炸)。
(task_goal / failure_context 两项经复核为误报,见 §1.2 v2.1 更正。)✅
验证:目标子集 99/99 通过;全量 `tests/unit + tests/tools` 1072 通过、
5 失败均为真实 LLM 联网测试的 API key 环境问题(与改动无关)。
**Phase 1 — State 显式化(仅动经台账核销的死键与信封)** ✅ 已实施 2026-08-31
实施要点(与下述计划的差异):信封迁移最终形态为**工具直接返回 ToolMessage**
(承载 content/status/tool_call_id,未传 state 时仍返回纯字符串),未引入独立的
ToolOutcome 类型——ToolMessage 已是 LangChain 原生形态,语义完全覆盖;
`_process_plan_write` 的拒绝路径返回 status="success" 的 ToolMessage 以逐字节保持
旧信封行为(SystemMessage 无 status 属性 → 旧路径视为 success)。visibility
manifest 已接线至全部 7 个节点入口(`strict_state`,默认关闭零开销),
契约测试 tests/unit/graph/test_state_contract.py 11 项覆盖往返/forbid/代理/清单。
验证:tests/unit 全量 1067 通过 0 失败;tests/tools 仅余 5 个既有 API key
环境失败(与 Phase 0 前完全一致)。测试改写含 test_operator.py 12 处
asanitize 断言 → 返回值断言、15 个工具测试文件 Command→ToolMessage。
- 删真死键:`messages`(同步改 `sdk/agent.py:1182-1187`、`:1321-1325`、
`graph_runner.py:57`、`_get_graph_state`)、`remaining_steps`、`focused_app_info`、
`device_date`、`subgoal_plan`、`operator_tactical_plan`、`complete_subgoals_by_ids`
(同步改 `operator.py:767` 与 `_get_graph_state`)、`current_agent`(scratchpad
两处读改固定 `"operator"` 或显式传参)、`operator_replan_reason`(committee 去引用)。
- **不删**:`short_term_memory`(活跃;移除并入姊妹文档 Phase 1 落地时执行)、
感知相关键(归宿在 Phase 2)。
- 信封迁移:14 个工具写入点 → `ToolOutcome` 直返;`operator.py:388-391` 与
`tool_wrapper.py:175-176` 拆信封逻辑简化;删 `validator_messages` 通道与
`asanitize_update`。
- `check_feedback` → `operator_feedback` 更名(operator prompt + diagnoser 两读者
同步)。
- `extra="forbid"` 上 State + `State(**values)` 往返测试(`sdk/agent.py:696` 路径);
visibility manifest + StateView 测试强制(先按附录 B 现状放宽,再收紧)。
**Phase 2 — PerceptionStore + TurnWorkspace**
- store/快照/引用/环形缓存;四个抓屏点收口(§5.1 表);消费者逐个切换;
- 删 `operator_raw_data`、`latest_ui_hierarchy`、`latest_screenshot`、
`indexed_points`、`indexed_elements`(State 通道侧);新增 `perception` ref 与
`workspace`;
- outputter 帧选择决策(post_action vs 决策帧)落地并记录。
**Phase 3 — ctx 分解**
`CheckpointLedger` / `PlanGovernor` / `BackgroundTaskGroup`;`run_outcome` 显式化;
`extra="forbid"` 上 ArtemisContext / DeviceContext / ExecutionSetup / AppLaunchResult。
**Phase 4 — 记忆内核**
`StepMemoryService` 统一运行时、双 lens 分立语义(Pro 侧 `StepCapsuleLens` 即
姊妹文档 Phase 2 的实现载体);策略表收拢(逐点对拍);配置收敛。之后按姊妹文档
继续 L3 分块与 recall。
每阶段独立可回滚。Phase 0/1 以台账+现有单测护航;Phase 2 起加对拍测试。
---
## 8. 验收
**不损伤(最低要求)**
- 附录 B 台账全表核销:每个字段/属性的每个消费者都有对应改动与测试;CI 中
visibility manifest 测试保证幽灵读为零。
- `State(**values)` 往返(`astream` values 流)在 `forbid` 下全绿。
- Flash:成功路径下压缩器输出与现版**逐字节对拍一致**;重试上界与 flush 属有意
变更,单独测试覆盖(失败注入下有界退出 + 降级态可见)。
- Pro:`build_plan_and_history` 策略表收拢后与现 6 处调用点输出逐点对拍一致。
- 性能红线沿用姊妹文档 §18(P50/P95 输入与延迟回归 ≤3–5%,关键路径零摘要等待)。
**优雅处理所有信息(目标)**
- 全部 `extra="forbid"`;抓屏管线唯一实现;图通道序列化不再含任何 b64
(对照:当前 `operator_raw_data` 每超步随 values 流复制)。
- Planner 驳回后,Operator 与 Diagnoser 的下一 prompt 可见驳回理由(新增回归测试)。
- validator 的 post-action 帧从"就地写路径后失名"变为可引用证据(store + DataEngine)。
- Pro 长任务(>30 步)历史出现视觉接地摘要;每个 agent 的输入面由 manifest +
ContextPolicy 两张表完整声明。
---
## 附录 A:agent → State 读/写实证清单(2026-08-31 工作区)
(visibility manifest 初始值;来源为本次全仓扫描。)
| 节点 | 读 | 写 |
|---|---|---|
| perception | structured_decisions | latest_screenshot, latest_ui_hierarchy, operator_raw_data, injected_instruction, user_stop_requested |
| planner | latest_screenshot, initial_goal | (无通道写;就地突变 latest_screenshot) |
| operator | operator_raw_data, latest_ui_hierarchy, indexed_elements, subagent_calls, current_step_id, initial_goal, check_feedback, short_term_memory, operator_tool_limit_exceeded, injected_instruction | structured_decisions, operator_raw_thinking, operator_native_thinking, short_term_memory, indexed_points, complete_subgoals_by_ids, current_step_id, subagent_calls, operator_tool_limit_exceeded |
| execution_check | operator_raw_data, structured_decisions, operator_*_thinking, user_stop_requested, initial_goal | checker_success, validator_messages, structured_decisions, current_step_id, check_feedback, subagent_calls |
| validator | structured_decisions, latest_screenshot, current_step_id, operator_raw_data, operator_*_thinking, indexed_points, indexed_elements | last_execution_result(+就地突变 latest_screenshot, indexed_*) |
| summarizer | current_step_id, structured_decisions, operator_*_thinking, last_execution_result | (无;异步写 DataEngine) |
| exit_settlement | user_stop_requested, initial_goal | exit_settlement_route, check_feedback, run_outcome |
| checker | (不读 State) | (不写 State;ctx.assert_halt + ledger) |
| diagnoser | initial_goal, check_feedback, latest_screenshot, latest_ui_hierarchy | (无) |
| outputter | initial_goal, operator_raw_data | (无) |
| explorer 工具 | operator_raw_data, latest_screenshot, latest_ui_hierarchy, indexed_* | (就地 append indexed_*) |
| failure_analyzer | latest_ui_hierarchy, initial_goal | (就地重推导 indexed_*) |
| committee 工具 | latest_screenshot, initial_goal | (无) |
| video/object_detector/log/history_analyzer | (不读或仅透传;object_detection 兜底读 latest_screenshot) | validator_messages(信封用法) |
## 附录 B:信息处置台账("不损伤"核销表)
### B.1 State 字段
| 字段 | 消费者(证据) | 处置 | 阶段 | 同步改动点 |
|---|---|---|---|---|
| initial_goal | 多节点读 | 保留 | — | — |
| injected_instruction | perception 写 / operator prompt 读 | 保留 | — | — |
| user_stop_requested | perception 写 / gate、checkpoints、plan 守卫读 | 保留 | — | — |
| checker_success | execution_check 写 / 两处 gate 读 | 保留 | — | — |
| check_feedback | graph.py:198,230,367 写 / prompts.py:243、diagnoser.py:178 读 | 更名 operator_feedback,扩容纳 planner 驳回 | P0(接入)+P1(更名) | 写 3 处、读 2 处、组件提示语 |
| run_outcome | graph.py:367 写 / sdk/agent.py:720 读 | 保留;ctx 侧动态挂载显式化 | P3 | sdk/agent.py:721 |
| exit_settlement_route | exit_settlement 写 / gate 读 | 保留 | — | — |
| current_step_id / structured_decisions / operator_*_thinking / last_execution_result / subagent_calls / operator_tool_limit_exceeded | 活跃读写对(附录 A) | 保留 | — | — |
| short_term_memory | operator.py:743-765 写 / prompts.py:331 读 | ~~**保留**;移除权属姊妹文档 §3.3~~ **已移除(M5,2026-09-01)**:全链核销——State 字段/operator 提取写块/prompt 组件/manifest 读写集/operator.json 指令段;渲染侧旧标签剥除保留 | — | — |
| messages | 写:无(Pro);读:sdk/agent.py:1182(空转)、:1321(空转);graph_runner.py:57 写(坏路径) | 删除 | P1 | 读 2 处、写 1 处、_get_graph_state |
| validator_messages | 14 工具信封写 + graph.py:195 跨回合写;通道 0 读 | 信封→ToolOutcome;:195→operator_feedback;删通道 | P0(:195)+P1(信封) | 14 工具 + tool_wrapper:175 + operator:388 + graph:526,595 |
| remaining_steps | 仅 init(sdk:1201);上限实际由 recursion_limit 承载(sdk:674) | 删除 | P1 | _get_graph_state |
| focused_app_info / device_date | 仅 init(sdk:1197-1198) | 删除 | P1 | _get_graph_state |
| subgoal_plan / operator_tactical_plan | 仅 init / 零引用 | 删除 | P1 | _get_graph_state |
| complete_subgoals_by_ids | operator:767 写 []、init、0 读 | 删除 | P1 | operator:767、_get_graph_state |
| current_agent | scratchpad:166,260 等读(恒回退 "operator"),0 写 | 删除 | P1 | scratchpad 固定值或显式传参 |
| operator_replan_reason | committee_tool:203 读(恒 None),0 写 | 删除 | P1 | committee 去引用 |
| latest_ui_hierarchy | perception 写 / operator:614、diagnoser:193、FA:419、explorer:1595 读 | → snapshot.fused_elements | P2 | 4 读者切 store |
| latest_screenshot | perception/planner/validator/flash 写 / validator:98、diagnoser:186、committee:138、explorer:268、object_detection:93 读 | → snapshot + capture 收口 | P2 | §5.1 表全部触点 |
| operator_raw_data | perception 写 / operator:610、validator:618、explorer:290、outputter:139 读 | → PerceptionRef + store | P2 | 4 读者切 store;outputter 帧选择决策 |
| indexed_points / indexed_elements | 节点内写读(operator/validator/FA/explorer/action_executor,附录 A) | → TurnWorkspace(节点局部) | P2 | §5.3 全部触点 |
| (幽灵)ui_tree | 4 读 0 写 | P0 止血→latest_ui_hierarchy ✅;P2 终态 | P0/P2 | operator:995、tool_declarations:477、action_executor:401、exec_tools:548 |
| (更正)failure_context | engine/BaseAgent 栈自有协议,`.get()` 有 isinstance 防护 | 误报,不动 | — | — |
| (更正)task_goal | `ExecutionContextState.task_goal` 存在(core/state.py:48) | 误报,不动 | — | — |
| (幽灵)metadata | sdk/types/task.py:223 对 finalize 快照 dict.get(恒 None) | 无害;随 finalize 清理顺手删 | P1 | 1 处 |
### B.2 ArtemisContext 属性
| 属性 | 消费者 | 处置 | 阶段 |
|---|---|---|---|
| latest_perception_data | 0 读(HEAD 版 checker 曾读,工作区已删) | 删除写入 + 注释 | P0 |
| run_outcome(动态) | sdk:1107 读 → trace 后缀 | 显式字段 | P3 |
| _active_driver | drivers/factory:75-78、tools/base:104、mock:55、4 处测试 | → PrivateAttr | P0 |
| pending_checkpoints / checkpoint_tasks / checkpoint_attempt_seq / checkpoint_repairs / final_check_attempts / assert_halt | graph/checkpoints.py 族 | → CheckpointLedger | P3 |
| task_plan_content_before / last_validated_plan / pending_validated_plan / planner_task | graph.py:_process_plan_write / execution_check | → PlanGovernor | P3 |
| background_tasks / background_jobs / grace_period | ctx.__aexit__、summarizer:66、diagnoser:234 | → BackgroundTaskGroup | P3 |
| 其余声明字段 | 基础设施 | 保留(分组) | P3 |
| (engine 侧)ExecutionContext.artemis_context | graph_runner:48、reactive_runner:57 读,0 写 | 显式接线或显式报错 | P0 |
-141
View File
@@ -1,141 +0,0 @@
# 历史模块统一方案 v3:最终真机验收报告(2026-09-01)
状态:已完成(真机 Pixel 10 / 59100DLCR0033X,worktree `zealous-gates-4626b1` 未提交代码,M0-M5+硬化轮全量、默认开形态)
对应任务:`docs/plans/history-module-redesign.md` §7 M5 要点"最终残留"中的真机三件套 + 默认开复测 A/B + recall include_images 端到端 + 相似度提示抽查(五项)。
---
## TLDR
在 Pixel 10 真机上以出厂默认配置(`agent.memory.transcript.enabled=true`)完成五项最终验收:**全部通过(样本量有限处为方向性结论)**。要点:
1. **默认开复测 A/B(4 对任务,同日同模型 gemini-3.6-flash)**:转写路径回退 **0 次**(11 个 flag-on 会话全程在岗);全回合延迟 P50 8.5s→3.4s(−60%)、缓存命中调用 40%→86%、输出 token −31%、成功率 4/4 vs 4/4 持平——上一轮 A/B 的收益方向在冷启动修复+就绪门控+并发 2 的最终形态下完整复现。
2. **段头就绪门控病理消失**:上一轮"21/21 chunk 段头全 pending"→ 本轮全部 flag-on 会话 **60/60 个 chunk 范围段头就绪**(就绪率 100%),关段→就绪落库(即交换时刻)中位 8–12s ≈ 1–2 回合。
3. **[Loop:continuous] 112 步长跑**:无摘要堆叠(6 era + 8 chunk 分层正常)、token 稳态有界(上下文基数峰值 26.3K = 预算 33%)、注入指令逐字保全(chunk ③ 与最终渲染提示中均在场)、release_loop 一次生效。
4. **capsule 盲评(48 个就绪段头全量机检 + 5 个人工精查)**:禁判定词违规 1/48("entered in the search bar",描述搜索框文字输入,语义无害);无编造;笔记关联与 ② 区间覆盖全部忠实。
5. **recall include_images 端到端打通**:data-URL 图片块(128KB)经 ToolMessage 进入 3 次连续 Gemini 调用全部 success,Operator 消费后写出正确笔记。
6. **相似度提示**:真实重访场景正确注入(指向 Step 2/4);同屏等待循环 60+ 步 0 次注入(近 3 步静默规则有效)。
**两条结论建议见 §8:flag 维持默认开;具备提交条件。**
必读偏差:主实验模型为 **gemini-3.6-flash**(3.7-flash 当日午间再次出现 503 风暴,见 §6-a;与 2026-09-01 基线报告同模型故纵向可比);`chunking.model` 保持 3.7-flash 且当轮 49 个 capsule 全部由 3.7 直接服务成功。`config/artemis.jsonc` 实验后已逐字节还原。
---
## 1. 方法与环境
- **代码**:worktree `C:\Users\wfq55\Documents\ChatGPT\artemis\.claude\worktrees\zealous-gates-4626b1`(M0-M5+硬化轮已实施未提交,含 A/B 轮的 operator.py 冷启动修复)。主检出 `.venv` Python、worktree 为 cwd,`python -m artemis.interfaces.cli.main run … --standalone -s 59100DLCR0033X -n <name> --session-id <sid> --without-video-recording-tools`。
- **设备**:Pixel 10(59100DLCR0033X),Android 16,中文界面。弹窗由 agent 自行处理。
- **模型**:default=google:gemini-3.6-flash(fallback 同 3.6);chunking=gemini-3.7-flash(capsule lens 带 3.6 降级链);step lens=gemini-3.5-flash-lite。当日 10:40 探针三模型全部健康,11:36–11:53 出现 3.7 的 503 窗口(§6-a)。
- **flag**:A 轮/Loop/recall/sim 用出厂默认(enabled=true 即 worktree 配置原文);B 轮临时置 false,跑完立即还原;实验结束后 `config/artemis.jsonc` 与实验前备份 diff 为空(含 default.model 还原 3.7)。
- **数据源**:worktree `traces/data_engine.db`(steps / traces:llm_call·llm_usage·log·tool / history_chunks),分析脚本在会话 scratchpad(analyze.py、swap_lag.py、dump_chunks.py、flagon/flagoff/loop_metrics.txt、chunks_dump.txt)。
- **统计口径**:P50/P90/P95 为跨会话池化分位数;"首调输入"= 每个 operator 回合的第一次 LLM 调用 input_tokens。
任务集(每任务 A/B 各一遍,同 goal 文本):
| 任务 | 内容概述 | 步数(on / off) |
|---|---|---|
| V1 settings-sweep | 设置 7 面板巡检,逐节写笔记+汇总 | 24 / 26 |
| V2 clock-alarms | 3 闹钟创建→核对→关 1→删 3→确认清空,逐阶段写笔记 | 55 / 38 |
| V3 multi-app-time | 时区+世界时钟 3 城添加读取移除+日历+笔记 | 19 / 20 |
| V4 mega-audit | 4 阶段综合巡检(设置 4 面板+世界时钟 2 城+闹钟 2 只+汇总),逐项写笔记 | 40 / 45 |
>30 步样本:flag-on V2(55)/V4(40),flag-off V2(38)/V4(45);V1/V3 在转写路径下被高效完成未达 30 步(与上一轮 T3 同型的任务设计偏差,长历史压力由 V2/V4 与 112 步 Loop 承载)。8 次 A/B 运行全部自然完成(rc=0,status=completed)。
## 2. 验收项 1:默认开复测 A/B
**flag-on(A 轮,4 会话)原始统计**:
| 会话 | 任务 | 步数 | 墙钟 s | 缓存命中调用 | cached/input | 首调输入 max | 段头就绪 |
|---|---|---|---|---|---|---|---|
| aaaa2001 | V1 | 24 | 551 | 80.6% | 50.5% | 21.3K | 3/3 |
| aaaa2002 | V2 | 55 | 504 | 86.8% | 53.2% | 29.6K | 8/8 |
| aaaa2003 | V3 | 19 | 210 | 82.8% | 54.0% | 22.6K | 3/3 |
| aaaa2004 | V4 | 40 | 383 | 90.2% | 48.1% | 22.5K | 4/4 |
**flag-off(B 轮,4 会话)**:bbbb3001-4,步数 26/38/20/45,墙钟 921/501/292/583s,缓存命中调用 26.4–45.3%,cached/input 16.8–37.3%。
**池化对照(重点指标逐条)**:
| 指标 | flag-off | flag-on | Δ |
|---|---|---|---|
| 回退旧路径次数 | —(旧路径本体) | **0**(且 11 个 flag-on 会话含 Loop/recall/sim 均 0;消息数跨回合正常累积) | ✅ 冷启动修复后转写全程在岗 |
| 段头就绪率 | — | **60/60 chunk 范围就绪(100%,全部 flag-on 会话)** | ✅ 上一轮 21/21 pending 病理消失 |
| 关段→就绪落库(=交换时刻,门控语义) | — | 中位 8–12s,max 65s(≈1–2 回合;ready 版本行在收割/交换时写入) | ✅ |
| 全回合延迟 P50/P90 | 8.5 / 21.0s | **3.4 / 8.0s** | −60% / −62% |
| 模型调用 P50/P90 | 4.1 / 11.6s | **3.0 / 6.4s** | −27% / −45% |
| 步间隔 P50/P90 | 12.7 / 23.5s | **7.5 / 12.3s** | −41% / −48% |
| Operator 缓存命中调用 | 93/234(40%) | **158/184(86%)** | ×2.2(不低于上一轮 88% 的量级) |
| 首调输入 P50/P95/max | 12.5K / 14.2K / 15.3K | 17.4K / 24.8K / 28.5K | +39%(≤预算 36%,符合转写保历史预期) |
| 输出 token 合计 | 236.0K(129 步) | **163.8K(138 步)** | −31%(按步 −35%) |
| 摘要退化回显率 | 0/125 | **0/134** | ✅ 单图变体修复后≈0(两侧同零:lens 修复不在 flag 后) |
| 摘要 failed | 0 | 0 | ✅ |
| 任务成功率 | 4/4 | 4/4 | ✅ 不劣化(笔记/删除收尾均在案) |
**A 区稳态**:flag-on 首调输入曲线有界(各会话 max 21–30K,远低于 80K 预算),chunk 交换处可见 >1.5K 的下降沿(详见 §3 Loop 曲线;短任务亦有 3.7K 级下降)。
辅助证据:换模型前用出厂 3.7-flash 完成的首轮 flag-on 三会话(aaaa1001-3,24/38/18 步)同样 0 回退、8/8 chunk 就绪、全部 completed——默认配置原样形态亦被验证过一遍(数据未入池化,避免混模型)。
## 3. 验收项 2:[Loop:continuous] 112 步长跑
会话 cccc2001:goal 明确要求 [Loop:continuous] 持续监控(电池+关于手机轮询,逐轮写 loop_monitor.md)。
- **规模与稳态**:112 步 / 879s(步间隔 P50 7.4s / P90 9.6s,全程无劣化趋势);回合 P50 3.6s / P95 6.3s。
- **token 稳态有界**:首调输入 11.3K(起)→ 26.3K(第 110 步附近,= 预算 33%),5 处 >1.5K 交换下降沿;净斜率 ≈130 tok/步,主导项为 ③ 账本逐行累积(外推软阈值 56K 需 ~340 步,之后依设计由 era ③ 溢出与 L3 接管——本轮未达该规模,未验证)。
- **无摘要堆叠**:最终渲染 F 区 = **6 个 era 块(各"merged from 1 chunks",① 结构化字段集合并 + ② 退化标题行 + ③ 逐步账本完整保留)+ 8 个全宽 chunk 块**,无"摘要的摘要"。15/15 chunk 段头就绪。
- **步骨架不变量**:最终提示中 ③ 账本行连续覆盖 Steps 1–98(era+chunk 两层均逐步可寻址),Steps 99–112 以原始消息在 A 区(99–105 已关段就绪、按门控待下次渲染交换;106–112 活跃窗口)——任何层级无丢步、无合并。
- **[Loop] 轮询记为计划性进度**:capsule 段头以"monitoring cycles / round 12、round 13"客观记录轮次,无异常标记(中立契约的 [Loop] 判别生效)。
- **注入指令 never-evict**:第 50 步注入的格式变更指令被 agent 当轮执行(后续笔记行含 [HH:MM] 时间戳),并以 `User @ Step 50: "…"` **逐字**保全于 chunk 50-56 的 ③(pending 与 ready 两个版本行均在),且在最终渲染提示中仍逐字在场。
- **release_loop**:第 112 步注入 release 信号后一回合内停止,session completed。
## 4. 验收项 3:capsule 盲评
样本:6 个会话的 **48 个就绪 chunk**(≥10 要求的 4.8 倍)全量自动机检 + 5 个跨场景人工精查(设置巡检段、闹钟创建段、世界时钟段、Loop 注入段、recall 首段)。
- **禁判定词**:全量扫描 band①② 命中 **1/48**——aaaa2004 chunk 13-19 exit_state 中 "'Singapore' entered in the search bar"。属描述搜索框内已输入文字的可观察状态,非导航/成功判定;按字面契约计 1 次违规,语义无害。其余 47 个 chunk 零命中(successfully/completed/failed/navigated to 等全部缺席)。
- **③ 账本对照**:人工精查的 5 段中 doing/did/effect 与 ③ 逐行动作、步级视觉摘要完全对得上;verified_facts 逐条可溯源到步摘要中的屏上文字(如 IMEI 356766283081582、toast "闹钟设置为 19 小时34 分钟后"逐字进账);无编造字段。
- **笔记要点摘录**:含笔记写入的段,effect 均点名目标文件并给出要点(如 "Appended two records to note 'settings_info_log': '电池电量: 100% (已充满)' and '设备名称: Pixel 10'")——机械校验(缺 key 判负重生成)在真机形态下工作。
- **② 区间叙事**:抽查段全部无缝覆盖(区间并集=段范围,生成时机械校验兜底)、同质动作并组正确(如 16–19 = 添加+选时+确认一组)、异质动作单步成行;叙事与实际步骤一致。
- 值得记录的正面细节:闹钟段 unresolved 正确写"Confirming the 21:30 alarm dialog"(段末选择器仍开着)——段头对"段内未完结事"的刻画准确。
## 5. 验收项 4/5:recall include_images 与相似度提示
**recall include_images(会话 dddd3001,82 步)**:阶段 1 浏览 3 个设置页后到时钟页等待;注入"回忆关于手机页内容,用 recall_history include_images=True 回捞截图对照确认"指令。结果:
- Operator 共调 recall_history 2 次(`include_images=true`,一次带 `step_range=[3,4]`),工具返回 `[text, caption("--- pre-action screenshot of Step 4 ---"), image_url(data:image/jpeg;base64, 128KB)]` 三块结构;
- 含图 ToolMessage 进入后续 **3 次连续 Gemini 调用全部 success**(input 20.7K/22.0K/22.1K,无格式/签名错误);
- Operator 消费后写出 recall_check.md:设备名称 Pixel 10、Android 版本、注明"来自 Step 4 截图"——**端到端 PASS**。
- 15/15 chunk 就绪、0 回退(该会话历史大部分已 chunk 化,recall 正是从冷历史回捞)。
**相似度提示(会话 dddd3002,11 步 + 交叉验证)**:
- 正例:电池→显示→声音→再回电池场景,Historical state hint 注入 10 次调用,指向 **Step 2(首次电池页)与 Step 4**,句式与方案原文一致,附 recall_history 指引;
- 静默负例:dddd3001 在时钟页等待轮询 60+ 步(同屏连续),hint **0 次**注入——近 3 步同屏静默规则有效,与像素级"卡同屏"组件未同时触发;
- 阈值 5 的行为观察:Loop 会话中 hint 在 106/112 次调用出现(轮询周期 7 步 > 静默窗口 3 步,每次重访都命中更早轮次)。成本一行文本无害,但可作为后续优化候选:活跃 [Loop] 里程碑内提示语义近于噪声(见 §6-e)。
## 6. 异常与发现清单
- **a.(环境)gemini-3.7-flash 午间 503 风暴**:11:36 起 planner/operator 连续 503 重试,首跑 acc-on-v4(aaaa1004)在 operator 180s 硬超时后 session failed(0 步入库,17 分钟耗在重试)。处置:按上一轮先例全轮切 gemini-3.6-flash 重跑(同日探针 3.6 三连 1.6–1.9s 健康),报告数据均为 3.6 同模型 A/B;`chunking.model` 维持 3.7——当轮 **49 个 capsule 全部由 3.7 直连成功**,硬化轮新加的 capsule 降级链真机上未被触发(其行为由单测覆盖,供应商窗口期的实弹验证留待自然发生)。3.7 作为出厂默认的可用性风险属供应商侧,与本方案无关,但建议留意。
- **b.(环境)设备 USB 掉线**:B 轮首次启动时设备从 adb 消失(4 个任务各 15s 内 ConnectError 退出,bbbb2001-4 弃用);adb 重启后短暂 unauthorized、约 1 分钟内自动恢复授权,换 sid 重跑成功。对数据无污染。
- **c.(行为观察,非缺陷)"等待用户指令"被建模为 [Loop:continuous]**:acc-recall 的 goal 说"完成后在页面等待用户进一步指令",planner 将其建模为常驻轮询循环;执行完注入指令后 agent 不自行退出(每 5–10s wait 轮询 + 逐轮更新 task_plan),需 release_loop 显式停止——与 plan grammar"[Loop] 只能被 release 信号终止"的设计一致,但此类措辞的任务会持续烧步数(本例 60+ 步等待轮询)。给使用侧的提示而非代码问题。
- **d.(口径)交换延迟的测量语义**:history_chunks 的 ready 版本行在"段头就绪后的首次渲染收割"时写库,因此 DB 上 close→ready 的 8–12s 中位数即"关段→交换"的端到端延迟(capsule LLM 生成时刻另见 lens:step_capsule llm_usage 计 49 条);"ready 行→首个含该 chunk 块的 llm_call"恒为 0s 与该语义吻合。
- **e.(优化候选,不阻塞)** [Loop] 任务中相似度提示近乎每回合触发(106/112);语义价值低。可选优化:活跃里程碑为 [Loop] 时静默或降频。
- **f.(样本量)** A/B 4 对 + Loop 1 + 场景 2,>30 步样本 4 个;未做置信区间,全部结论按方向性措辞。§8 红线"30 对任务、置信区间"的完整统计功课依旧未做——但两轮独立 A/B(上一轮 6 对 + 本轮 4 对)方向完全一致。
- **g.(工作区)** 实验新增 traces/(15 次运行)为 gitignored;`config/artemis.jsonc` 已还原(与实验前备份逐字节一致);无任何 artemis/ 源码改动。
## 7. §8 验收红线逐条判定
| 红线(方案 §8) | 判定 | 依据 |
|---|---|---|
| 不损伤(压缩可逆、回捞可用、失败回退可用) | **PASS** | recall 从已 chunk 化冷历史回捞文本+图片成功消费(§5);60/60 段头就绪故 pending 降级梯本轮未被迫使用;摘要 failed 0 |
| 步骨架不变量(任何压缩层级步 1:1 可寻址) | **PASS** | 112 步长跑最终提示:era/chunk ③ 连续覆盖 1–98、99–112 原文在场;era 合并后 ③ 逐段完整;注入指令逐字保全(§3) |
| 速度(关键路径零摘要等待;延迟回归 ≤5%) | **PASS(方向性)** | dispatch 零阻塞;全回合 P50 −60%、P90 −62%——大幅改善而非回归;样本 4 对 <30 对 |
| 缓存(命中率提升必须量化) | **PASS** | 同日同模型 A/B:命中调用 40%→86%,op cached/input 17–37%→48–54% |
| 质量(重复动作率/回滚率不升、冲突趋零) | **PASS(方向性)** | 成功率 8/8 持平;退化摘要 0/259(两轮合计);盲评无摘要-事实冲突;重复动作率未单独统计(Loop 任务的重复为计划性,段头正确标注轮次) |
| 对拍(辅助视图收拢、Flash M1 语义等价逐字节) | **PASS(引用)** | M4/M5 golden 单测锁定(tests/unit 1229+3 全绿,2026-09-01 M5 轮验证);本轮未重跑单测、无代码改动 |
## 8. 结论与建议
1. **flag 维持默认开(`agent.memory.transcript.enabled=true`)**。依据:本轮为冷启动修复+就绪门控+max_concurrency=2 的最终形态下、与基线轮同模型的独立复测——延迟减半、缓存命中翻倍、输出 token −31%、成功率持平、0 回退、0 退化摘要、段头就绪率 100%;上一轮遗留的三项病理(每回合静默回退、21/21 pending、单图回显)全部实测消失。`enabled:false` 回滚开关本轮实际使用(B 轮)且行为正确(0 chunk、旧 2-message 路径),回滚通道可信。
2. **具备提交条件**。M0-M5+硬化轮的全部真机验收项完成且无阻塞缺陷;worktree(含 operator.py 冷启动修复、1229+3 单测)建议按仓库纪律走提交/PR(用户决定时机)。不阻塞提交的遗留优化两项:§6-e([Loop] 内相似度提示静默)、§6-c("等待指令"类措辞的使用侧文档提示);另建议对 3.7-flash 的供应商稳定性保持观察(§6-a)。
-129
View File
@@ -1,129 +0,0 @@
# 历史模块真机基线与 M2 A/B 实测报告(2026-09-01)
状态:已完成(真机 Pixel 10 / 59100DLCR0033X,worktree `zealous-gates-4626b1` 未提交代码)
对应任务:`docs/plans/history-module-redesign.md` §7 的 M0 未完成项(基线录制)与 M2 待真机项(A/B 对拍)。
---
## TLDR
在 Pixel 10 真机上,用冻结的 3 个代表性长任务(各 2 对样本,共 6 基线 + 6 flag-on 运行,全部 Pro profile)完成了 M0 基线录制与 M2 转写路径 A/B。**结论(方向性,样本量 6 对):§8 全部红线通过,且转写路径远超"不回归"标准——全回合延迟 P50 从 21.4s 降到 5.0s(−77%),模型调用 P50 13.8s→4.3s,缓存命中调用占比 43%→88%(token 口径 29.8%→49.6%),总输出 token −74%,任务成功率两轮均 6/6,flag-on 轮零回退、零摘要等待、零摘要 failed、无思考签名/消息格式类错误。**建议按 §10 决策 3 推进灰度,但先复测一轮确认(见"重要注意事项")。
三个必须显著标注的事项:
1. **发现并最小修复了一个 M2 阻塞 bug**(operator.py 冷启动误判,导致转写路径从第 2 回合起永久静默回退旧路径)。首轮 flag-on 数据因此作废重跑。修复未提交,在 worktree 工作区中,需 M2/M3 会话认领合入。
2. **A/B 的处理组不是纯 M2**:worktree 中已有 M3 代码(`HistoryChunkManager` 挂同一 flag),flag-on 轮实际是"M2 转写 + M3 L2 chunking"叠加(6 会话共产生 21 个 chunk)。收益归因到 M2/M3 的拆分需要后续单独对拍。
3. **实验模型为 `gemini-3.6-flash`**(配置默认的 3.7-flash 当日对本 key 持续 503),两轮同模型可比;`config/artemis.jsonc` 已还原原文。
---
## 1. 方法与环境
- **代码**:worktree `C:\Users\wfq55\Documents\ChatGPT\artemis\.claude\worktrees\zealous-gates-4626b1`(分支 `claude/zealous-gates-4626b1`,M0+M1+M2 已实施未提交;另含 M3 chunking 代码)。以主检出 `.venv` 的 Python、worktree 为 cwd 运行 `python -m artemis.interfaces.cli.main run … --standalone`(绕开外部 Daemon,确保跑的是 worktree 代码;`import artemis` 解析与 ROOT_DIR/traces 均已验证指向 worktree)。
- **设备**:Pixel 10(59100DLCR0033X),Android 16,中文界面。`--without-video-recording-tools`,checker/committee/planner_validation 均按仓库配置关闭;两轮环境完全一致。
- **模型**:default=google:gemini-3.6-flash(原因见 TLDR #3;fallback 同 3.6);lens=gemini-3.5-flash-lite(可用性已验证)。
- **flag**:基线轮 `agent.memory.transcript.enabled` 缺省(=false);A/B 轮在 worktree `config/artemis.jsonc` 临时置 true,实验后已还原。
- **数据源**(worktree `traces/data_engine.db`):
- `llm_call` trace(DataEngineCallbackHandler):单次模型调用 duration、消息数、token_usage(含 `input_token_details.cache_read`)、父 agent span 归属;
- `agent` span trace:operator 回合(=全回合延迟)、planner/validator/summarizer;
- `llm_usage` trace(M0 token_meter):网关出口计量 + 会话累计;
- `steps` 表:步时间戳(步间隔)、`extra_metadata.summary_status/…`(M0 版本化字段,全程在案);
- 摘要就绪延迟 = 视觉 lens 调用(summarizer span 派生)的结束时刻 − dispatch span 时刻(配对按 FIFO 顺序,近似值)。
- **统计口径**:P50/P90/P95 为跨会话池化分位数;分析脚本与原始输出保存在会话 scratchpad(`analyze.py`、`base-metrics.txt`、`flagon2-metrics.txt`)。
- **看门狗**:单任务 45 分钟 timeout,任务间 10s 间隔;全部任务均未触发 timeout。
## 2. 冻结任务集(Pro profile,多里程碑)
| 任务 | 目标概述 | 无副作用性 |
|---|---|---|
| T1 settings-sweep | 设置 7 个面板逐项采集(关于本机/电池/显示/声音/网络/存储/日期时间)并逐节追加到笔记,最后汇总 | 纯读 + 内部笔记 |
| T2 clock-alarms | 时钟创建 3 个闹钟(6:15/7:45/21:30)→ 逐个核对 → 关闭其一 → 逐个删除 → 确认清空 | 自建自删 |
| T3 multi-app-time | 设置读时区 → 时钟世界钟加 London/Tokyo 读时间后移除 → 日历读今日日期 → 笔记比对汇总 | 自加自删 + 纯读 |
实测步数:T1 26–45 步、T2 28–45 步均达 >30 步目标区间;T3 14–27 步偏短(多应用切换任务被 agent 高效完成,未达 30 步——记为任务设计偏差,不影响 T1/T2 承载的长历史压力)。12 次运行全部自然完成(rc=0、状态 completed),无人工干预;设备侧弹窗由 agent 自行处理。
## 3. 基线轮原始统计(flag 关,6 会话)
| 会话 | 任务 | 步数 | 墙钟 s | Operator 首调输入 tok P50/P95 | 缓存命中调用 | cached/input | 模型调用 s P50/P90 | 回合 s P50/P90 | 步间隔 s P50/P90 | 摘要就绪 s P50/P90 | 摘要 failed |
|---|---|---|---|---|---|---|---|---|---|---|---|
| db83934e | T1 | 34 | 1415 | 11305/12204 | 23/59 | 26.6% | 22.0/32.5 | 29.2/60.2 | 33.2/63.2 | 189.2/275.3 | 0 |
| 1f229af1 | T2 | 40 | 1560 | 12107/17034 | 13/52 | 16.0% | 26.9/41.3 | 30.1/55.9 | 33.5/52.5 | 107.7/215.5 | 0 |
| e31a052d | T3 | 14 | 729 | 11646/12805 | 14/29 | 32.6% | 8.0/25.1 | 17.4/56.1 | 21.4/59.8 | 2.3/11.4 | 0 |
| f02e473f | T1 | 45 | 1375 | 15879/18558 | 72/109 | 45.3% | 6.9/23.0 | 15.3/48.5 | 18.2/52.2 | 25.5/106.3 | 0 |
| 765305ba | T2 | 39 | 1098 | 12701/14272 | 6/46 | 8.3% | 15.2/32.2 | 15.6/33.9 | 19.0/37.8 | 127.4/198.7 | 1 |
| 87b3d0e6 | T3 | 16 | 634 | 12808/13918 | 10/28 | 22.7% | 9.6/29.1 | 17.4/69.6 | 22.8/91.5 | 29.3/93.0 | 0 |
**基线聚合(这是 §3.4/§10 定标的正式基线)**:
- Operator 首调输入 token:**P50 = 12,190,P95 = 17,589**(legacy 2-message 全量重编译路径)
- Operator 缓存:命中调用 **138/323(43%)**,cached/input = **29.8%**(Gemini 隐式缓存对稳定提示头已有基础命中)
- 模型调用延迟:**P50 = 13.8s,P90 = 31.2s**;全回合:**P50 = 21.4s,P90 = 58.0s**;步间隔:**P50 = 24.8s,P90 = 62.2s**
- 摘要就绪延迟(dispatch→ready):**P50 = 60.0s,P90 = 213.6s**(长尾主因:`max_concurrency=1` 串行队列 + lens 25s 超时重试;见 §5-d)
- 摘要 failed 率:**1/188(0.5%)**;任务成功率 6/6
- 会话级(6 会话合计):LLM 调用 349 次,prompt 4.61M tok,completion 263.6K tok
## 4. A/B 轮(flag 开 = M2 转写 + M3 chunking)与红线对照
有效 A/B 轮(flagon2,operator.py 修复后)原始统计:
| 会话 | 任务 | 步数 | 墙钟 s | 首调输入 tok P50/P95 | 缓存命中调用 | cached/input | 模型调用 s P50/P90 | 回合 s P50/P90 | 步间隔 s P50/P90 | 摘要就绪 s P50/P90 |
|---|---|---|---|---|---|---|---|---|---|---|
| 5867d1f2 | T1 | 26 | 345 | 16158/19551 | 28/30 | 50.4% | 4.9/8.0 | 5.3/8.7 | 8.9/11.4 | 44.9/74.2 |
| bebd7329 | T2 | 28 | 480 | 15588/20225 | 26/30 | 48.0% | 4.2/40.4 | 4.4/33.6 | 8.4/37.7 | 25.3/65.5 |
| 8f9cf01b | T3 | 14 | 344 | 16174/20191 | 22/27 | 51.7% | 3.3/8.6 | 6.0/14.6 | 8.2/18.0 | 2.9/5.8 |
| fed69c93 | T1 | 39 | 608 | 15218/17104 | 44/50 | 51.7% | 5.3/21.0 | 6.7/22.8 | 9.7/26.7 | 17.3/64.0 |
| 4b426449 | T2 | 40 | 635 | 17154/21287 | 38/43 | 46.4% | 4.0/32.6 | 4.5/24.0 | 8.3/27.0 | 35.2/61.2 |
| 59a9613f | T3 | 18 | 508 | 15789/19905 | 26/29 | 50.2% | 3.2/7.8 | 4.0/13.2 | 8.1/26.7 | 82.0/330.3 |
**红线对照(方案 §8 / 姊妹文档 §18)**:
| 红线 | 基线 | flag 开 | 判定 |
|---|---|---|---|
| 模型调用延迟回归 ≤5% | P50 13.8s / P90 31.2s | **P50 4.3s(−69%)/ P90 22.8s(−27%)** | ✅ 大幅改善而非回归 |
| 全回合延迟回归 ≤5% | P50 21.4s / P90 58.0s | **P50 5.0s(−77%)/ P90 21.4s(−63%)** | ✅ 同上 |
| 缓存命中显著上升 | 43% 调用 / 29.8% token | **88% 调用 / 49.6% token** | ✅(这是本方案核心收益,已量化) |
| 关键路径零摘要等待 | —(旧路径无此概念) | 回合 P50 5.0s,无任何等待摘要的阻塞形态;dispatch span 恒 <0.1s | ✅ |
| 无思考签名/消息格式错误 | — | 12 会话日志与 log trace 扫描(signature/format/malformed/invalid message 关键词)零命中;503 重试 0–3 次/会话属供应商限流 | ✅ |
| 转写异常回退次数为零或有明确原因 | — | **修复后 6 会话 0 次**(修复前每回合都回退,原因明确且已修,见 §5-a) | ✅ |
| 任务成功率不劣化 | 6/6 | 6/6(笔记调用 14–20 次/任务、删除等收尾动作在案,完成质量抽查可信) | ✅ |
**附加观察(非红线但值得记录)**:
- 首调输入 token P50 +32%(12.2K→16.1K):转写保留全部历史消息,符合预期;绝对值仍远低于 80K 预算。
- 总输出 token **−74%**(263.6K→68.7K)、LLM 总调用 −34%(349→231):真多轮下模型不再每回合重新推理全局,重复思考被消除——这是延迟改善的主要来源之一(另一来源是缓存命中提高了 TTFT)。
- 墙钟:T1 1415/1375s → 345/608s;步间隔 P50 24.8s→8.8s。
- M3 chunking 实际触发:6 会话共 21 个 HistoryChunk(里程碑切换 + 尺寸边界,步区间 1:1 无丢步,最大 7 步/块);③ 机械账本全部就位,**①② LLM 段头全部停留 pending**(后台生成未落地,属 M3 未完成面,移交 M3 会话)。
## 5. 异常与发现清单
**a.(阻塞级,已最小修复——需 M2/M3 会话认领)M2 冷启动误判 bug**:`operator.py::_build_prompt_transcript` 的冷启动检查(`ledger.turn_count == 0 and steps`)在第 2 回合必然误命中——上一回合只 stage 未 commit,`turn_count` 仍为 0 而 steps 已非空,`set_restored_history` 被账本的空账本不变量拒绝抛异常,触发灰度安全网**每回合静默回退旧路径**。实测表现:首轮 flag-on 6 会话全部 operator 首调恒为 2 条消息、每会话 ~30 条 "Transcript prompt path failed … Restored history can only seed an empty ledger" 错误日志。**首轮 flag-on 数据作废**(它实测的是"旧路径+每回合异常开销")。最小修复:冷启动条件增加 `not ledger.has_staged_turn`(operator.py,含注释共 +8 行);修复后 68 个相关单测全绿,验证运行零回退、消息数跨回合正常累积(2→7→11→…)。**测试缺口**:`test_operator_transcript.py` 的"两回合四区结构"用例未覆盖"第 2 回合时 steps 非空"的真实形态,建议补一例。
**b.(质量级,两轮同等存在)Pro 步记录后图恒缺 → 视觉 lens 单图退化**:Pro 的 `record_step` 中 `post_image_name` 恒为 None(步 N 的后图即步 N+1 的前图,记录时不存在独立后图),M2 SummarizerNode 从 DataEngine 取图 dispatch 时 post 恒为 None,视觉 lens 只收到前图。flash-lite 在单图输入下高频退化——直接回显分隔符原文(`--- [2] AFTER ACTION SCREEN ---`)当作摘要。实测退化率:基线 52/181(29%)、flag 开 55/159(35%)。flag 开时这些坏摘要会在擦洗沿替换截图进入 Operator 上下文,是当前转写质量的最大缺口。建议(M3 前修):lens dispatch 延迟到下一步感知就绪后,用步 N+1 的前图充当步 N 的后图;或 lens 契约对单图输入改为"描述前屏与动作意图"并在 prompt 中显式声明无后图。
**c.(计量缺口)Flash/Pro 视觉 lens 调用不经 LLM 网关**:`VisualStepSummarizer` 用 `get_google_llm` 原始模型(`self._llm.ainvoke`),绕过 `RobustChatModelWrapper` → **无 llm_usage 计量条目**(本轮 995 条 llm_usage 全部为 google:gemini-3.6-flash,无 flash-lite 来源)。M0 实施要点第 3 条"raw 模型旁路仅测试使用"的表述与现状不符。lens 成本目前只能从回调 llm_call trace 旁证。建议 lens 切网关或单独接计量。
**d.(长尾)摘要就绪延迟远超擦洗窗口**:基线 P50 60s / P90 214s;flag 开 P50 23.6s / P90 72.9s(转写降低了主调用排队干扰后明显改善)。长尾成因:`memory.runtime.max_concurrency=1` 串行队列 + 单次 25s 超时 + 有界重试。flag 开后步间隔 P50 仅 8.8s,K=3 的擦洗沿深度 ≈26s:P50 恰好赶上,P90 会吃满宽限(3 步 ≈26s)并落占位符。对定标的含义见 §6。
**e.(环境)gemini-3.7-flash 当日持续 503**(实验前探测 5/5 失败),整个实验固定用 3.6-flash;`config/artemis.jsonc` 已按备份还原为 3.7 原文。绝对延迟数字换模型后不可直接外推,A/B 相对结论不受影响。
**f.(样本)** T3 实测 14–27 步未达 30 步设计目标;每任务 2 对的样本量下所有结论按方向性措辞,未做置信区间。§8 "30 对任务、置信区间"的正式验收仍待更大样本。
**g.(工作区)** 实验在 worktree 留下的痕迹:`.env`(从主检出复制,gitignored)、`traces/`(12+3 次运行的 db 与 trace 目录,gitignored)、operator.py 最小修复(见 a)。`config/artemis.jsonc` 已还原。基线含 1 例摘要 failed(765305ba,M0 有界重试耗尽进显式 failed 态,符合设计)。
## 6. §3.4 临时默认值定标建议(基于本轮实测,供 M3/M4 采用)
| 参数 | 临时值 | 实测证据 | 建议 |
|---|---|---|---|
| `context_budget_tokens` | 80K | 40+ 步长任务转写路径首调输入 P95 ≈ 20.4K、最大 23.4K(含活跃窗口内截图);稳态远低于预算 | **维持 80K**。当前形态下 ~100 步内不会触及软阈值;若未来放开图片保留深度或加大 XML 保留再复标 |
| 软/硬阈值 | 0.7 / 0.9 | 本轮从未接近(≤29%),未获得触发数据 | **维持 0.7/0.9**(无证据要求调整;触发行为的验收留给 M3 的 `[Loop:continuous]` 100+ 步试跑) |
| `image_scrub_depth` | 3 | flag 开步间隔 P50 8.8s → K=3 ≈ 26s 深度;摘要就绪 P50 23.6s、P90 72.9s → P50 恰好赶上、P90 吃满宽限落占位符 | **维持 K=3**,但配套二选一:`memory.runtime.max_concurrency` 1→2(压就绪长尾,lens 是独立 flash-lite 调用,并发 2 风险低),或 `pending_grace_steps` 3→5。优先前者(占位符是信息损失,宽限只是延迟损失) |
| `min_active_steps` | 5 | 本轮 L2 触发时活跃窗口底线未被逼近(chunk 最大 7 步、上下文远未满) | **维持 5**(无反证数据) |
| chunk 触发 ≥12 步 / ≥2K token | 12 / 2000 | 21 个 chunk 实际由里程碑切换主导(1–7 步/块),尺寸兜底未成为主触发器 | **维持**。里程碑触发工作正常;尺寸兜底的检验需要单里程碑长段任务(`[Loop:continuous]`),留 M3 验收项 |
## 7. 结论与建议
1. **M0 基线已录制**(§3 表格即正式基线),M2 A/B 待真机项已完成,红线全过(方向性)。
2. **转写路径收益远超预期**:延迟 −63%~−77%、缓存命中翻倍、输出 token −74%、调用数 −34%,且成功率持平。"真多轮 + 缓存命中"的架构判断被实测强烈支持。
3. **灰度建议**:flag 具备进入默认开的候选资格,但建议满足两个前置后再翻默认:① §5-a 修复被 M2/M3 会话正式认领合入(含补测试);② 修复后再跑一轮 A/B 确认(本轮 A/B 的处理组叠加了 M3 chunking 与 operator.py 修复,归因不纯)。
4. **移交 M3 会话**:chunk ①② 段头 pending 未落地、lens 单图退化修复(§5-b)、lens 计量接入(§5-c)、`max_concurrency` 调参(§6)。
-988
View File
@@ -1,988 +0,0 @@
# ARTEMIS 历史模块重构:增量转写 + 分层压缩(统一方案 v3)
状态:v3 已实施并通过最终真机验收(2026-09-01,M0-M5 全部落地;最终验收五项全过,见 `docs/plans/history-acceptance-2026-09-01.md`;仅 git 提交待用户决定)
范围:Operator 执行历史的表示、累积、压缩与召回;Flash / Pro 双 profile 收敛为同一记忆运行时。
取代关系:
- **取代** `docs/design/pro-context-memory-redesign.md`(2026-08-25)的 §6/§11/§12(分层历史、token 预算、缓存布局)——其"每回合重编译 + 预算选择"的前提被本方案的"增量转写"取代;其 §7(数据模型)、§8(视觉接地中立摘要)、§9(稳定分块)、§10(recall)被**吸收并保留**。
- **修订** `docs/plans/context-visibility-redesign.md` 的 §6(方案 D 记忆内核)——StepMemoryService 统一运行时保留,但"两 lens 按 profile 分立"修订为"两 lens 按高度分立"(见 §5,决策变更已获用户确认,2026-08-31)。该文档的 Phase 0/1(State 显式化)已实施,Phase 2(PerceptionStore)/Phase 3(ctx 分解)不受本方案影响,Phase 4 由本方案取代。
---
## 0. 用户提案复述与结论
提案三条(2026-08-31):
1. **每步与 Flash 一致,仅做增量构建**;后台用 Gemini 3.5 Flash-Lite 做图片压缩(视觉摘要替代截图)。
2. **里程碑边界压缩**:不再每步压缩,而是在**切换里程碑时**把上一段任务的历史压缩掉;保留现方案的滑动窗口底线(永远保留最近几步原始消息)。
3. **上限兜底**:长期不切换里程碑时,按最大上下文窗口限制触发压缩。
4. **(补充约束,2026-08-31/09-01 两轮明确)段级胶囊要特别详细、尽量维持原状、按步压缩,且定形为三段式**:① 梗概与效果(在做什么/做了什么/有什么效果,含留下的笔记)→ ② 压缩步摘要("Steps xx–xx 做什么、Step xx 做什么"区间叙事)→ ③ 逐步动作账本(每步一行:步号+时间+动作)。理由:视频分析模块按步号/时间戳对齐视频时间轴,且大模型对任务整体的时间感知必须精确到步。落点见 §3.3。
**结论:方向成立,且与业界最优实践收敛。** Claude Code、Codex、Gemini CLI 全部采用"累积 + 阈值/边界触发压缩",无一采用 ARTEMIS Pro 现在的"每回合全量重编译"(后者的直接代价:跨回合零消息连续性、零提示缓存命中、思考签名丢失)。姊妹文档把"真多轮消息"排在 Phase 5,本提案把它提前为核心架构——这是正确的提前。
在采纳的同时做**四点修正/补强**(正文展开):
| # | 提案原文 | 修正 | 理由 |
|---|---|---|---|
| 1 | "后台每步做图片压缩" | 摘要**每步后台准备**,但替换**延迟到固定深度批量应用**(尾部倒数第 K 步才把图换成摘要) | 就地改写第 i 字节起的缓存前缀全部失效;固定深度擦洗让前缀单调增长(Anthropic computer-use 官方同款权衡,见 §2.5) |
| 2 | "切换里程碑时压缩之前的任务" | 里程碑切换当**触发器**,压缩块的**身份**按 step_id 范围锚定,里程碑 hash 仅作标签元数据 | 计划是动态索引不是历史键(姊妹文档 §9 实证结论):计划改写/回滚/重命名不得破坏已压缩历史 |
| 3 | "按最大上下文窗口限制压缩" | 需要真实计量——现在全仓**没有任何 token 计量**(探索报告 §8);用**上一轮 `usage_metadata.prompt_tokens` 作为当前上下文的实测值** + 增量启发式,零额外成本 | 字符估算对图片/多模态误差大;上一轮实测输入即本轮上下文基数 |
| 4 | (未提及) | 补两层:**紧急全量快照**(阈值兜底也爆时,Codex/Gemini CLI 式结构化快照重启)与 **recall_history 无损回捞**(DataEngine 冷历史,压缩永远可逆) | 无界任务(`[Loop:continuous]`)会耗尽任何有限分层;无损回捞是"敢压缩"的前提 |
---
## 1. 现状基线(2026-08-31 工作区实证)
### 1.1 Flash:append-only + 每轮就地压缩
- 消息列表建一次、只追加,**无任何消息驱逐**,唯一边界是 `max_turns=30`(`runner.py:166,177`)。
- `compress_flash_messages`(`context_compressor.py:43`)每轮 LLM 调用前全量重扫:只保留**最后一张**图(keep-last-1,`:62-72`);历史 UI XML 从 `--- UI Element List ---` 标记截断(`:133-139`);图片"摘要未就绪则保留"(lossless-pending,`:141-146`);摘要以 `--- Historical Visual Transition ---` 前缀块注入 ToolMessage,幂等重建(`:125-128`)。
- 已知缺口:摘要**迟到回填**会突变任意深度的历史消息(缓存敌意);图已被丢而摘要仍未到时该步**既无图也无摘要**(race,`:144` 只保护仍在场的图)。
- `VisualStepSummarizer`(`flash/summarizer.py:39`):flash-lite、前图叠加红色动作标记+后图、**无界重试**(`_retry_delays` 末档 3s 永续循环,`:105`)、单次 25s 超时、成功后写 `data_engine.update_step_summary`(`:205`)。
- token 仅做事后记账(char/4 + 258/图启发式,`runner.py:293-315`),**从不驱动压缩**。
### 1.2 Pro:每回合全量重建,零跨回合消息
- `_build_prompt` 每回合新建 PromptBuilder,产出恰好 2 条消息(System + Human),回合结束整表丢弃(`operator.py:161-218,705-715`;`prompts.py:139-154`)。**跨回合连续性只剩 `plan_and_history` 渲染串 + `short_term_memory`**;思考签名、原生多轮全部丢失;提示缓存对易变尾部零命中(网关无任何 cache_control/cached_content plumbing,`llm.py` 全文)。
- 历史可见性 4 策略散布 8 个调用点硬编码 kwargs(`task_tree.py:797-942` + 探索报告 §2.4 全表);`strict_milestone` 是硬悬崖——活跃里程碑外且最近 N 步外的历史**完全消失,无摘要残留**(`:859-876`)。
- 摘要未就绪的降级:Flash 保图,Pro 回退整步 detailed 渲染(`task_tree.py:920`)——两者都是"多花 token 等摘要",但均不可观测。
- 两个 live bug:Diagnoser 传不存在的 `window_steps=` 参数 + 不存在的 `history_window_steps` 配置,异常被吞,**Diagnoser 的历史每次都渲染成错误串**(`diagnoser.py:160-174`);`build_plan_and_history` 两个死参数(`chronological_last_step`/`detailed_subgoal_hashes`)4 个调用点仍在传。
### 1.3 存储与事件面
- `StepRecord` 无 `summary_status`/`version`/`chunk` 概念;`update_step_summary` 是后台盲覆写,无版本无序保证(`engine.py:974-1005`)。扩展逃生口是 `extra_metadata`(已承载 `subgoal_hash`/`width`/`height`/`token_usage`)。
- 里程碑切换的现成检测点:① 每步已盖章 `extra_metadata["subgoal_hash"]`(`graph.py:128`),相邻步 hash 变化即切换;② `_process_plan_write`(`graph.py:505-615`)是全部计划写入的单一漏斗,已在计算 `milestones_changed`/`new_top_level_completions`。重命名韧性由 `subgoal_hash_chain.json` 别名链保证(`task_tree.py:991`)。
- 现成接缝:operator 模板里 `unified_history=""` 是**已接线的空渲染槽**(`prompts.py:213`);`PromptComponent` 是干净插件点;`MAX_MESSAGES_IN_HISTORY=25` 是死常量(`constants.py:16`)。
---
## 2. 业界机制要点(2026-08 调研,取数字用于定标)
### 2.1 Claude Code:三层递进
① **微压缩**(每次调用前、无模型参与):清掉旧 tool_result,只留最近 3–5 个,占位符替代,原文落盘可回捞;② **服务端 context editing**(`clear_tool_uses`):默认 100K 触发、keep 3,关键参数 `clear_at_least`——**不值得为小清理打破缓存就不清**(批量摊销失效成本的官方形态);③ **全量压缩**:~95% 用量触发,9 节结构化摘要(意图/技术要点/文件/错误与修复/用户消息/待办/当前工作/下一步),压缩后重注入最近读过的 ~5 个文件。另有 memory tool:清理阈值临近时警告模型先把重要结果写进记忆文件。
### 2.2 Codex CLI:极简两件套 + 原生压缩
阈值 ≈ 有效窗口 − 13K(用户自设值被钳制在窗口 90% 以内)。压缩后**只剩两样**:一条摘要消息(进度/关键决策/约束/剩余工作)+ 最近 ~20K 原始用户消息;旧摘要被折叠进新摘要,**永不堆叠**。压缩后自动重读最近编辑的 ≤5 个文件(50K 预算)。GPT-5.1-Codex-Max 起模型**原生训练了跨窗口压缩**。UI 明示"多次压缩会降低准确性"。
### 2.3 Gemini CLI:70/30 + 结构化快照
历史达模型窗口 **70%** 触发;**最近 30% 原样保留**(切分点保证不劈开 tool-call/response 对);更旧的压成 `<state_snapshot>` XML(overall_goal / key_knowledge / file_system_state / recent_actions / current_plan),压缩率 5–15%。
### 2.4 Manus KV 缓存法则(生产指标)
agent 输入:输出 ≈ 100:1,缓存命中是**第一生产指标**(cached $0.30 vs uncached $3.00/MTok)。法则:前缀字节级稳定(系统提示禁时间戳)、**append-only**(永不修改既往动作/观察、序列化确定性)、工具 mask 不删除、**文件系统即外部记忆**(可恢复地压缩:丢网页留 URL,丢文件内容留路径)、todo 复诵对抗 lost-in-the-middle、错误留在上下文里。
### 2.5 截图历史(computer-use 官方指引,与本仓直接同构)
Anthropic:截图每张 ~1,000–1,800 token,单请求 ≤20 张;**推荐保留最近 3 张,更旧的每 ~25 轮批量修剪**——明确说明逐轮修剪会每轮打破前缀缓存,批量修剪让失效事件之间前缀字节不变;被逐出的截图用一句文本摘要替位。参考实现即 `only_n_most_recent_images` 参数。OpenAI CUA 则靠服务端 `previous_response_id` 携带历史,客户端只上传新帧。
### 2.6 Gemini 缓存约束(本仓运行时)
隐式缓存 2.5+ 默认开启;**最小可缓存前缀 2,048(2.5 系)/ 4,096(3.x 系)token**,命中报告在 `cachedContentTokenCount`;条目短寿命,相似前缀请求要时间上贴近。显式 `CachedContent` TTL 默认 1h、按 token-hour 计存储费(explorer.py:1709 已有先例)。**任何更早字节的改动使其后全部失效**——这是修正 #1 的硬依据。
### 2.7 提炼的六条通用原则
1. 累积为常态,压缩靠**阈值/边界稀疏触发**,不逐步进行。
2. 突变要么贴尾(浅、可预测),要么稀疏成批(深、与语义边界重合)。
3. 摘要输出**结构化 schema**,不是自由散文;旧摘要折叠进新摘要,不堆叠。
4. 最近 K 步/最近 30% 永远原样——所有系统都有生窗口下限。
5. 压缩必须**可逆**:原文外置(磁盘/DataEngine),占位符携带回捞引用。
6. 计量用真实值,阈值分软/硬两档,硬档换更激进的形态而非同一手段加量。
---
## 3. 目标架构:单一转写(Transcript)+ 四区纪律 + 三级压缩
### 3.1 核心转变
Operator 的上下文从"每回合重编译的渲染串"变为**会话级增量转写**——一份 append-only 的消息账本,Flash 与 Pro 共用同一实现,由新服务 `TranscriptLedger`(挂 `ctx.step_memory` 之下或并列)持有。State 只携带轻引用,重物不进图通道(与 visibility 方案 C 同一原则)。
`build_plan_and_history` **不删除**,降级为两个角色:
- **辅助 agent 的编译视图**:Diagnoser / Committee / Outputter / HistoryAnalyzer / Planner / Checker 仍按各自 ContextPolicy 从 DataEngine 编译(Claude Code 同型:主循环累积、子 agent 拿新鲜编译上下文);
- **转写的冷启动构造器**:进程重启/崩溃恢复时,从 DataEngine 步骤记录重建转写的冻结区。
### 3.2 四区纪律(缓存友好的突变规则)
```text
[S 稳定前缀] 系统提示 + 工具 schema + 计划语法
整会话字节不变;≥4K token 以满足 Gemini 3.x 隐式缓存下限
[F 冻结历史] 已压缩产物:chunk 摘要块 + 逐步摘要行
只在"压缩事件"时突变(稀疏、深)
[A 活跃窗口] 最近 N 步原始消息(AI 推理原文 + 工具结果)
append-only;倒数第 K 步处有一个"擦洗沿"(浅、恒定深度)
[T 当前尾部] 本回合观察:当前截图 + UI 列表 + 计划复诵 + 注入指令
每回合新建
```
**突变只允许三种,全部可预测:**
1. **贴尾擦洗(深度恒定,逐回合)**:
- 深度 1:上一回合的 UI XML 列表、计划复诵副本剥除(现 Flash `prune_history_xml` 行为,代价只是最后一回合的缓存,几乎免费);
- 深度 K(默认 3):倒数第 K 步的截图 → 替换为后台已备好的视觉转换摘要。摘要通常在几秒内就绪,K 步 ≈ 数十秒,几乎总能赶上;未就绪则该步暂缓(lossless-pending 保图),最多宽限 m 步(默认 3),仍未到则替换为占位符 `[visual summary pending; evidence at DataEngine step N]`——**彻底消灭现 Flash 的"迟到回填突变任意深度"与"图丢摘要空"两个缺陷**。
- 恒定深度擦洗让缓存前缀**单调增长**(每轮失效的只是最后 K 步),同时 token 稳态恒定——严格优于"每 25 轮批一次"(后者在批间攒 25 轮的图)。
2. **压缩事件(稀疏、深)**:见 §3.3,F 区重写 + A 区前移。深突变与语义边界重合,此时接受一次全量 re-prefill(Claude/Codex 同款代价)。
- **思维签名政策(2026-09-01 定):回合内强制、A 区顺带、压缩即弃**——Gemini thought signature 仅在当前回合函数调用周期内是 API 硬要求(原样回传);A 区 AIMessage 字节不动故签名免费有效(擦洗只碰观察/工具消息,从不碰 AIMessage);已完结回合的签名 API 层面可选,chunk 化整回合移除后自然消亡,不建任何跨压缩维护机制(签名绑定消息精确内容,语义上不可能脱离原消息存续)。真机 A/B 无签名相关错误佐证。
3. **紧急快照(罕见)**:F 区整体替换为会话快照,见 §3.3 L3。
### 3.3 三级压缩
**L1 — 图片/XML 擦洗**(上文深度 K 机制)。摘要由 `StepMemoryService` 每步后台 dispatch(flash-lite,沿用红标叠加 + 禁判定词契约),**准备是每步的,应用是延迟的**。
**L2 — 段压缩(chunk 化)**,触发器取先到者:
- **里程碑切换**:**唯一事实源是已执行步骤的 `subgoal_hash` 实际变化**(`_record_turn` 处比较相邻步盖章);`_process_plan_write` 的顶层完结事件只入队一个"待确认边界",由下一个盖章步骤确认后才触发——防止被棘轮验证否决回滚的计划写入造成伪切换。切换时把上一段(不含活跃窗口底线内的步)压成一个 **HistoryChunk**;
- **尺寸阈值**(不切里程碑时的兜底,即提案第 3 条):开放段累计 ≥12 步或段内摘要 ≥2K token;
- **边界事件**:包/Activity 大切换、目标变更、FA 边界、不可逆副作用(继承姊妹文档 §6.5 全表)。
Chunk 身份 = `(start_step_id, end_step_id, source_step_ids)`;`subgoal_hash`(经别名链解析)只是标签。
**chunk 是"分段账本",不是融合胶囊(用户硬约束,2026-08-31)**:压缩只缩每步的**宽度**(推理原文/XML/截图 → 摘要/动作行),永不缩步的**数量**——段内步 1:1 保留,不合并、不重排、不丢步。两个刚性理由:① 视频分析模块以步号/时间戳为外键对齐视频时间轴,步粒度一旦融合即断链;② 大模型对任务整体的时间感知必须精确到步("第 23 步做了什么、距今多少步"要可直接回答)。
**chunk 内部结构为三段式(用户 2026-09-01 定形:梗概 + 效果 + 压缩步摘要,底下逐步动作)**:
```text
[Chunk 3 | 里程碑「登录并进入设置页」| Steps 18–29 | T+12:40 → T+15:02]
① 梗概与效果(LLM 段头)
本段在做什么:为后续改配置,先完成登录并进入设置页。
实际做了什么:用账号 A 走了短信验证码登录,途中关闭了一次推广弹窗。
效果/留下什么:已登录态(账号 A);验证码输入页出现过一次超时重发;
笔记 notes/login_flow.md 记录了验证码入口路径。
Entry: 应用主页,未登录 Exit: 设置页已打开
Verified: … Unresolved: … Failed paths: … Entities: …
② 压缩步摘要(LLM,逐区间叙事,必须带步号且全覆盖)
- Steps 18–20: 进入登录页,切换到验证码登录并填入手机号
- Step 21: 首次获取验证码后等待期间出现推广弹窗,关闭之
- Steps 22–26: 输入验证码,一次超时后重发并重新输入
- Steps 27–29: 登录后从侧边栏进入设置页
③ 逐步动作账本(机械拼装,1:1,永不省略)
- Step 18 (T+12:40): tap(632,1180)[登录入口] → executed
- Step 19 (T+12:56): tap(210,884)[验证码登录] → executed
- Step 20 (T+13:04): type("138…") → executed
…(每步一行,直到 Step 29)
```
三段的分工与生成方式:
- **① 段头梗概(LLM,gemini-3.7-flash)**:三问式散文(在做什么 / 做了什么 / 有什么效果——效果必须包含本段**留下的笔记**:段内 notes 写入/更新的文件与要点引用)+ 结构化字段(沿姊妹文档 §7.3:verified_facts / unresolved / failed_paths / important_entities / entry_state / exit_state)。要求**特别详细、尽量维持原状**:宁可长,不许把事实抽象到不可辨认。**笔记关联是机械校验项(用户 2026-09-01 定,"不能丢")**:段内每条 note 写入(save/update/append_note,经 trace 抓取)的目标文件 key 必须出现在段头 effect 中,缺任一即该次尝试判负重生成——与 ② 的覆盖校验同级;chunk 存"指针+要点",笔记全文在 notes 系统(read_note/recall 可回捞),可恢复压缩。
- **② 压缩步摘要(LLM,同一次调用产出)**:"Steps xx–xx 做了什么,Step xx 做了什么"式的区间叙事,输入是段内逐步视觉摘要 + 动作 + Validator 结果。硬性约束:每行必须带步号引用;**区间并集必须无缝覆盖整段步范围**(机械校验,缺口即重生成);同一区间只并列同质连续动作,异质动作单步成行;中立措辞契约沿用(禁判定词)。
- **③ 逐步动作账本(机械拼装,不经 LLM,零失真)**:步号 + 相对时间 + 精确动作 + Controller/Validator 结果短语,全部从 DataEngine 确定性渲染。这是视频对齐与步级时间感知的**硬骨架**,任何层级的压缩都不得触碰。三条账本细则:
- **动作必须带元素语义(用户约束,2026-09-01)**:`tap(632,1180)['确认按钮']` 而非裸坐标。索引点击路径已在记录时存 `target_text/resource_id/class`(`operator.py:893-896`);裸坐标路径(`operator.py:854-861` 返回全空)与 Flash/FA 动作工具需补**记录时语义充实**(见 §6.1);坐标永远保留(视频对齐/重放的外键),语义并列。
- **FA 恢复子动作入账**:一步内 Failure Analyzer 补做的真实设备动作(`_exec_*`,已在 `interleaved_events`)以缩进子行机械渲染——视频轴上有帧的动作,账本必须有行。
- **用户注入指令逐字保全**:段内 `injected_instruction` 以独立行原文保留在 ③(`User @ Step 21: "…"`),任何压缩层级 never-evict(Codex 保用户消息 / Claude Code 摘要设 All User Messages 节,同一共识)。
每步的完整视觉摘要不再逐条出现在 chunk 里(它们是 ② 的生成输入,原文永远在 DataEngine,recall 可回捞;A 区未压缩前照常在场)。chunk 版本化 append-only。
**就绪门控交换(用户 2026-09-01 定,"必须就绪再换";修订 M3 原实现)**:段头 ①② 未就绪时**禁止**交换——原始消息原样留在转写里,lossless-pending 原则从图片层贯彻到段层。边界事件只做两件事:关闭段、dispatch 段头生成;**交换发生在段头就绪之后的下一次渲染/压缩事件**(深突变时机照旧成批,缓存代价不变,只是延后)。理由:段头缺席时 `verified_facts` 缺席,只剩"发生了什么"没有"确立了什么"——模型会怀疑已验证事实并重做,这是实测过的死循环高危形态。降级梯:生成失败(有界重试耗尽)→ 原文继续保留 + fallback 模型重投;软阈值压力下仍未就绪 → 依旧保留原文(正确性优先于 token);**仅硬阈值紧急态**允许 ③-only 交换(带 pending 标记与 recall 指引;checker post-hoc 标注独立于段头,pending 态照常渲染)或直接走 L3。M3 原"冻结 ③ + pending 标记、下次事件收割"的实现按此修正——A/B 中"21 chunk 段头全 pending"在门控下退化为"尚未压缩"的安全形态,而非"历史只剩账本"。
**旧 chunk 不堆叠新摘要**:无界任务(`[Loop:continuous]`)下 chunk 数超限时做二级合并(era 合并)——**合并的只是 ① 段头**(结构化字段集合并,非散文再摘要),② 区间叙事与 ③ 逐步账本仍按段保留(预算极紧时 ② 可退化为只留区间标题行)。可见预算靠分级降宽(全三段 → ①合并+②③ → ①合并+③ → 时段段落)+ recall 兜底。
**极限层(决策 5 终审定案,用户 2026-09-01):允许丢步,压成"时段段落"**——era 数再超上限时,最老 era 整体压缩为一段话,**必须携带步号范围与起止时间**:`[Era N | Steps a–b | T+hh:mm → T+hh:mm] <一段话概括该时段在做什么/做了什么/留下什么>` + recall 指引行;用户注入指令行仍逐字保留(never-evict 不随丢步豁免)。段落**机械拼装优先**(由该 era 各 chunk ① 的 doing/did/effect 与 verified_facts 浓缩,不做散文再摘要的 LLM 调用;段头缺失的 chunk 回退用里程碑标签)。步级可寻址在此层降级为"时段可寻址":起止时间是视频对齐的粗粒度外键,步级细节走 recall_history 回捞(DataEngine 永远完整)。此前"步骨架任何层级不破"的表述据此修订:**该不变量适用于 L2 chunk、era 合并、L3 快照,唯极限时段段落层除外**。
**L3 — 紧急全量快照**:实测 token 越过硬阈值(如连续注入大量工具输出)时,F 区的**段头/知识面**整体替换为一个结构化会话快照(schema 取 Gemini CLI `state_snapshot` 与 Claude Code 9 节的交集,加本域字段):`overall_goal / plan_state / verified_facts / unresolved / failed_paths / important_entities / device_app_state / recent_actions`。快照从 chunk 段头结构化字段合并生成,尽量不做"摘要的摘要"。**逐步索引仍保留**(最小行宽:步号 + 相对时间 + 动作短语),步级时间骨架在最坏情况下也不断链。A 区底线不动。
**滑动窗口底线(提案第 2 条保留)**:任何触发器都不得把 A 区压到 `min_active_steps`(默认 5)以下;最近 1–2 个完整回合(AI 推理原文 + 工具结果 + Validator 配对)永远原样——姊妹文档 §4.2"连续推理先于压缩"原则不变,且切分点永不劈开 tool-call/response 对(Gemini CLI 同款约束)。
### 3.4 计量与阈值
- **实测优先**:每轮把上一次 LLM 调用的 `usage_metadata.prompt_tokens` 记为转写基数,加本轮追加内容的启发式增量(char/4 + 每图按分辨率估算)作为决策输入。无估算器依赖、无额外调用。
- 预算三档(**临时默认值,M0 实测后定标**):`context_budget_tokens=80K`;软阈值 0.7(56K)→ 触发 L2 压最旧段;硬阈值 0.9(72K)→ L3 快照。工具单次输出上限 16K(Codex 同款),超限走同回合信封(姊妹文档 §13 保留)。
- 姊妹文档实测提醒仍有效:8.8–12.7K 区间"长度与延迟无关"**不可外推**到 80K;但缓存命中改变了成本函数——M0 必须重测缓存命中下的 TTFT 曲线再定 budget。
### 3.5 可逆性与召回
- 一切被压缩内容在 DataEngine 原样在案(现状已满足:截图 SHA-256 内容寻址、步骤/trace 全量);占位符与 chunk 块一律携带 step 引用。
- `recall_history` 工具照姊妹文档 §10 原样落地(FTS + step 范围 + 包/Activity + 屏幕 hash,≤5 结果 ≤2K token,图片按需回捞单步);本地屏幕相似度提示(不发图、不调模型)保留。
- **Operator 只挂一个历史工具(2026-09-01 定,防过度设计)**:取历史 = chunk ①②③ 常驻索引(免费)→ `recall_history` 确定性回捞(一次查询,事实级)。HistoryAnalyzer **不进 Operator 工具清单、不进提示词指导**——它输出属推理级(事实优先级链最末)、每次调用是全量历史输入 + 关键路径等待,且"开放式跨历史语义问题"的需求目前纯属推测。仓库中它照旧存在(其他流程自用);若真机 trace 日后出现"recall 反复查不到、问题属聚合/因果型"的实证,再评估以升级路径接入。era 溢出标记行指向 recall。
---
## 4. 与提案逐条对照
| 提案 | 方案落点 |
|---|---|
| 每步增量构建(Flash 式) | §3.1 Transcript:Pro/Flash 统一 append-only;Pro 获得真多轮 + 思考签名 + 缓存命中 |
| 后台 flash-lite 图片压缩 | §3.3 L1:每步后台准备(StepMemoryService dispatch),深度 K 延迟应用(修正 #1) |
| 里程碑切换时压缩前段 | §3.3 L2:里程碑切换是首要触发器;chunk 身份按 step 范围(修正 #2) |
| 滑动窗口底线 | §3.3:`min_active_steps` + 最近完整回合硬保留 |
| 长期不切换按窗口上限压 | §3.3 L2 尺寸阈值 + §3.4 实测计量(修正 #3);再兜底 L3 快照(补强 #4) |
---
## 5. 摘要语义:两个 lens 按"高度"分工(决策修订,已确认 2026-08-31)
visibility 文档 §6.1 曾定"两 lens 按 profile 分立"(Flash=证据替身,Pro=整步胶囊),依据是 *Pro 丢弃旧步思考*。**本方案改变了这个前提**:转写保留推理原文直到段压缩,Pro 的步级摘要也变成了"证据替身"。因此修订为:
- **步级 = `VisualTransitionLens`(唯一步级 lens,双 profile 共用,输入按可得自适应)**:**替换哪张图就描述哪张图,有什么用什么**(用户 2026-09-01 定)——Flash 前后双图照旧;Pro 仅单决策帧+动作红标(描述该屏内容与动作落点,转换信息由相邻步摘要自然链接),单图时提示词不含 AFTER ACTION 段。**Pro 无动作后抓屏是刻意安全网**:ADB 截图延迟会丢瞬态控件(实测视频播放控件),截图量已压到最低——不得以任何方式"修复"(不补抓、不用步 N+1 前图拼后图)。禁判定词契约不变(successfully/completed/entered/navigated/failed/... 全禁);缺后图永远只表示"无独立后验证据"。
- **段级 = `StepCapsuleLens` 产出 chunk 的 ①+②**:意图/策略/进度的压缩发生在段压缩时,输入是段内逐步视觉摘要 + 动作 + Validator 结果 + 该段推理原文(压缩前最后一次可读)+ 段内 notes 写入记录。单次调用产出 ① 梗概与效果(三问式散文 + 结构化字段,含留下的笔记)与 ② 压缩步摘要(步区间叙事,步号全覆盖机械校验)。**③ 逐步动作账本机械拼装不经 LLM**(用户硬约束:按步压缩、尽量维持原状、时间感知精确到步)。`summarizer.md` 的语义权威与中立契约(含 `[Loop]` 循环不算异常的判别)迁移到 ①②。
- **会话级 = 快照 schema**(L3 专用)。
- 事实优先级链不变:用户指令 > Validator/Controller 客观结果 > XML/OCR 差异 > 有源摘要 > 推理。
推论:Pro 图中的 `SummarizerNode` 不再逐步生成文本胶囊,退化为 `dispatch(VisualTransitionLens)` 一行;辅助 agent 编译视图中的步行改为"时间 + 视觉摘要 + 精确动作 + Validator 结果",段外历史提供 chunk digest。**这是对先前已确认决策的修订,用户已于 2026-08-31 拍板采纳(见 §10 决策 1)。**
---
## 6. 数据模型与图接线
### 6.1 DataEngine 增量(最小 schema 变更)
- `StepRecord.extra_metadata` 增写:`summary_status`(pending/ready/failed/stale)、`summary_source`(lens 名)、`summary_version`、`summary_model`——`update_step_summary` 从盲覆写升级为带状态与版本的写入(同键并发以版本判序)。
- 新增 chunk 存储(SQLite 新表,字段即 §3.3 结构 + `session_id`/`version`);`get_agent_friendly_steps` 增配套 `get_history_chunks`。
- 修 `record_step` 的隐式语义:post==pre 时 `post_image_name=None` 表示"无独立后图证据",摘要器**不得**据此推断"屏幕未变"(姊妹文档 §4.3 原则写进 lens 契约)。
- **动作语义充实(record-time enrichment,M0)**:动作落 DataEngine 前,凡带坐标而缺 `target_text` 的(裸坐标点击 `operator.py:854-861`、Flash 动作工具、FA `_exec_*`),对当帧感知快照做命中测试(fused_elements → XML → OCR 取覆盖该点的最小可交互元素),best-effort 填 `target_text/target_class/target_resource_id`,并记 `target_label_source: "index"|"hit_test"|"ocr"|"none"` 区分模型指名与事后推断。渲染侧 `format_action_clean`(task_tree.py:39-64)已支持 target_text,零改动受益。Phase 2 PerceptionStore 落地后命中测试即快照一次查询,零额外抓取。
### 6.2 运行时归属
```text
ctx.step_memory: StepMemoryService # visibility 方案 D 保留:调度/有界重试/flush/step_id 键控
├─ lenses: VisualTransitionLens / StepCapsuleLens(chunk) / SnapshotLens
└─ transcript: TranscriptLedger # 新:四区消息账本 + 擦洗沿 + 压缩事件
```
- 有界重试(替换 Flash 无界循环)+ `flush(timeout)` 收尾 + 显式降级态,均照 visibility §6.1 原文。
- 转写在内存;崩溃恢复走 §3.1 冷启动构造器。素材以 `PerceptionRef`/`image_name` 引用(Phase 2 落地后免搬 bytes;Phase 2 未落地前先传 bytes,接口留 ref 形态)。
### 6.3 图接线
- **事件源**:`_record_turn`(`graph.py:108-148`)比较相邻步 `subgoal_hash` → `transcript.on_milestone_boundary()`;`_process_plan_write` 的 `new_top_level_completions` → 同一入口(去重)。
- **Operator 切换**(flag `memory.transcript.enabled`):`_build_prompt` 改为 `transcript.render_messages(tail=当前观察)`;旧 2-message 路径完整保留为回退。现成接缝 `unified_history` 槽位用于过渡期 A/B 对拍。
- **Flash 切换**:`compress_flash_messages` 的逐轮全量重扫替换为擦洗沿推进;`max_turns` 可放宽(压缩已兜底上下文)。
- 辅助 agent 不动图结构,编译视图接 ContextPolicy 策略表(visibility §6.2 原样,8 个调用点逐点对拍)。
- 顺手修:Diagnoser `window_steps` bug、两个死参数、死常量 `MAX_MESSAGES_IN_HISTORY`。
### 6.4 配置(收敛 visibility §6.3 与姊妹文档 §14)
```jsonc
"agent": {
"memory": {
"runtime": { "max_concurrency": 2, "retry_limit": 3, "flush_timeout_s": 30 }, // max_concurrency 1→2:硬化轮定标(基线报告 §6)
"transcript": {
"enabled": true, // M2-M4 出厂关灰度;M5 起默认开(2026-09-01 用户拍板);false 即逐字节回滚开关
"context_budget_tokens": 80000, // 2026-09-01 基线定标:维持(稳态 ≤29% 预算)
"soft_ratio": 0.7, "hard_ratio": 0.9,
"min_active_steps": 5,
"image_scrub_depth": 3, "pending_grace_steps": 3,
"xml_scrub_depth": 1,
"similarity_hint": true, "similarity_max_distance": 5 // M5 定标 8→5(460 步 dHash 分布)
},
"chunking": { "max_steps": 12, "target_source_tokens": 2000, "model": "gemini-3.7-flash", "max_chunks": 8, "max_eras": null }, // max_eras null=随 max_chunks(M5 新独立键)
"step_lens": { "model": "gemini-3.5-flash-lite" }, // 现 agent.flash.step_summarizer 键保持兼容别名
"recall": { "enabled": true, "max_results": 5, "max_text_tokens": 2000 },
"policies": { /* 辅助 agent ContextPolicy 覆盖 */ }
}
}
```
---
## 7. 分阶段落地
前置说明:visibility 文档 Phase 0/1 已实施;其 Phase 2(PerceptionStore)/Phase 3(ctx 分解)独立推进,与本轨道仅在 §6.2 的 ref 传递处交汇(非阻塞依赖)。
**M0 — 计量与 schema(无行为变化,可独立合入)** ✅ 代码侧已实施 2026-08-31;✅ 基线录制已完成 2026-09-01(真机 Pixel 10,见 `docs/plans/history-baseline-2026-09-01.md`)
- `summary_status/source/version` 落 `extra_metadata`;`update_step_summary` 版本化;
- 动作语义充实(§6.1):裸坐标/Flash/FA 动作记录时命中测试补 `target_text` 等字段(越早上线历史数据越完整);
- 转写计量管线(上轮 prompt_tokens 采集 + 增量估算)纯记录不决策;采集缓存命中率基线(`cachedContentTokenCount`);
- 修 Diagnoser bug、死参数、死常量;Flash 摘要重试改有界 + flush 语义(visibility §8 已定为有意变更,单测覆盖);
- 冻结代表性长任务集,录基线:输入 token、TTFT、全回合延迟、摘要就绪延迟、重复动作率、FA 触发率(姊妹文档 Phase 0 原样)。
M0 实施要点(2026-08-31,工作区未提交;与上述计划的差异逐条标注):
1. **摘要状态与版本化**:`StorageManager.update_step_summary` 升级为原子读改写
(单连接内读 `extra_metadata` → 判序 → 写回),签名
`(step_id, summary, *, source, version, model, status)`,返回 bool(stale 写返回
False)。判序规则:显式 `version` 低于在库版本即丢弃;`version=None` 自动
`在库+1`;**状态降级需显式更高版本**——`pending` 不覆盖 `ready/failed`、
`failed` 不覆盖 `ready`(防后台竞态回退);`summary` 列仅被
`ready`+非 None 摘要触碰。`DataEngine.update_step_summary` 同签名透传(后台
写 + 仅 ready 发 SSE,与旧行为逐字节一致)。调用方:Flash
`VisualStepSummarizer` dispatch 时补写 `pending`、成功写
`ready/source="visual_transition"/model`、耗尽写 `failed`;Pro `SummarizerNode`
写 `ready/source="step_capsule"/model`。`stale` 态本阶段只入枚举,无写入方
(M2 起由重摘/回滚使用)。
2. **动作语义充实(record-time enrichment)**:新模块
`artemis/utils/element_hit_test.py`(`find_element_at_point` /
`hit_test_semantics`:最小覆盖元素胜出,XML 源优先于 OCR 源,best-effort 永不
抛异常)。三条落点:① Pro 裸坐标分支(`operator.py` `resolve_target_element`
坐标路径)就地命中测试 `state.indexed_elements`,click/long_press/input_text
动作 dict 增 `target_label_source`(索引路径盖 `"index"`);②
`McpActionExecutor._execute_device_action`(Flash 与 FA 共用漏斗)在
`_translate` 后、设备调用前对归一化 target 命中测试(**必须在 observe 之前
——observe 会覆写 indexed_elements 为后帧**),语义并入 action trace 的 args
payload(FA 的 interleaved_events 渲染 `format_tool_call_clean` 零改动受益)并经
`ToolExecutionResult.metadata["target_semantics"]` 回传;③ FlashRunner 落
`record_step` 前把该 metadata 合入 `action_dict` 顶层。与计划的差异:
`click_sequence`/`swipe` 多点动作本阶段不充实(单目标动作先行);"可交互"
以"已被感知索引"近似(索引元素不带 clickable 位)。
3. **token 计量管线**:新模块 `artemis/services/token_meter.py`
(`extract_usage` 统一 LangChain `usage_metadata` 与 Gemini 原生
`cachedContentTokenCount`/`input_token_details.cache_read` 两种形态;
`SessionTokenMeter` 按会话累计)。采集点接在 LLM 网关
`RobustChatModelWrapper.complete()/ainvoke()` 出口(全部生产调用必经;raw
模型旁路仅测试使用,不采集),每次调用记一条 `llm_usage` trace(type
`llm_call`):单次 prompt/completion/cached + `context_base_tokens`(=本次实测
prompt,即下一轮上下文基数)+ 会话累计五项(calls/prompt/completion/cached/
cache-hit calls)。纯记录,不驱动任何决策;采集失败静默降级永不进入调用路径。
4. **三缺陷修复**:① Diagnoser `get_recent_subgoal_hashes` 改传
`base_dir`(对齐 committee_tool 用法),Diagnoser 历史渲染恢复正常;②
`build_plan_and_history` 删除 `chronological_last_step`/`detailed_subgoal_hashes`
死参数,operator/failure_analyzer/planner 3 个调用点与 2 处单测同步清理;③
删死常量 `MAX_MESSAGES_IN_HISTORY`(constants.py,全仓零引用确认)。
5. **Flash 摘要重试有界化**(有意行为变更):`_run_summary_until_ready` 以
`1 + retry_limit` 次尝试封顶(`StepSummarizerConfig.retry_limit`,默认 3,
FlashRunner 接线),耗尽进显式 failed 态(`has_failed()` + DataEngine
`summary_status="failed"`);**failed 步在压缩器视角仍为 pending**(保图,
lossless 语义不变——变更只砍掉无限 LLM 重试,不改消息流);`flush(timeout)`
收尾语义保留。
6. **测试**:新增 `tests/unit/utils/test_element_hit_test.py`(6)、
`tests/unit/data_engine/test_step_summary_versioning.py`(7,含并发判序/降级
拒绝/SSE 仅 ready)、`tests/unit/test_token_meter.py`(7)、
`tests/unit/mcp/test_action_executor_semantics.py`(4,含缺元素数据优雅降级
`"none"`)、operator 命中测试正例 + Flash 有界重试失败注入 2 例;存量断言
同步(operator 精确 dict 断言补 `target_label_source`、两摘要器调用断言补
kwargs)。验证:tests/unit 全量 **1094 通过 3 跳过 0 失败**;tests/tools
相关子集(operator/diagnoser/validator)7 通过(既有 5 个 API key 联网失败
未触发)。
7. ~~**未完成项**:代表性长任务基线录制~~ **已完成(2026-09-01)**:3 个冻结
长任务 ×2 样本真机录制完毕,正式基线与 §3.4 定标建议见
`docs/plans/history-baseline-2026-09-01.md` §3/§6(要点:Operator 首调输入
P50 12.2K/P95 17.6K;隐式缓存基线 43% 调用/29.8% token;模型调用 P50
13.8s;摘要就绪 P50 60s/P90 214s——长尾超擦洗窗口,定标建议
max_concurrency 1→2)。附带发现:视觉 lens 调用走 `get_google_llm` 原始旁
路,无 llm_usage 计量(与本条"raw 旁路仅测试使用"表述不符,待收口)。
**M1 — StepMemoryService + 擦洗沿(先 Flash 落地验证)** ✅ 代码侧已实施 2026-08-31(缓存命中提升待真机实测)
- 共享运行时成形;Flash 从"逐轮全量重扫"切到"擦洗沿"(深度 K 图替换 + 宽限 + 占位符);
- 验收:成功路径消息序列与现版语义等价(图→摘要时点后移属有意变更,单独对拍);缓存命中率显著上升(有实测基线可比);race 缺陷回归测试(图丢摘要空、迟到回填深突变均不再发生)。
M1 实施要点(2026-08-31,worktree `zealous-gates-4626b1` 未提交,叠加在 M0 之上;与计划的差异逐条标注):
1. **StepMemoryService 成形**:新 `artemis/memory/step_memory.py`——调度骨架自
Flash `VisualStepSummarizer` 上提:零阻塞 `submit`、有界重试(1 + retry_limit,
沿 M0 语义,耗尽进显式 failed 态并触发 `_on_status` 钩子)、`flush(timeout)`
收尾(无参时用配置的 `flush_timeout_s`)、`max_concurrency` 信号量封顶并发
(重试退避 sleep 在信号量之外,不饿死同伴)、状态查询
(has_job/is_pending/has_failed/get_summary/get_job_payload)。**键控**:有
DataEngine step_id 时以 `str(step_id)` 为规范键,tool_call_id 落别名映射;两者
皆无的旧调用方保持序号键(压缩器的 tool_call_id → 序号回退链原样保留)。
`VisualStepSummarizer` 取"并入服务"路线——改为服务子类,只保留视觉 lens 语义
(红标叠加、`flash_summarizer.md` 禁判定词契约、M0 版本化
`summary_status` 写入),对外表面(dispatch 签名/查询方法)零迁移。与计划的
差异:a) 服务本阶段仍由 FlashRunner 自持,未挂 `ctx.step_memory`(M2 Pro 切换
时再上提组合根);b) lens 未做成独立协议对象,视觉 lens 即子类 `_attempt`
(StepCapsuleLens 落地时再抽形式化 lens 接口);c) `max_concurrency=1` 默认值
使摘要 LLM 调用串行化(旧实现无界并发),属计划 §6.4 既定值。
2. **擦洗沿压缩器**:`ScrubEdgeCompressor`(`context_compressor.py`)——索引制
旁路账本(契约:FlashRunner 消息列表 append-only),增量发现新消息,冻结前缀
永不重扫。纪律:深度 1 剥 XML(保留集 = 最新 `xml_scrub_depth` 条观察,合并块
保前缀语义与 `prune_history_xml` 开关照旧);深度 K 图片决议——ready→摘要块
(沿用 `--- Historical Visual Transition ---` 前缀、剥图后追加于消息尾,与旧
产物同形)、failed→立即 `[visual summary unavailable; evidence at DataEngine
step N]`(**不吃宽限**,M0 的"failed 仍保图"按计划收口为有意行为变更,单测
覆盖)、pending→保图宽限至深度 K + pending_grace_steps,越限则
`[visual summary pending; evidence at DataEngine step N]`;宽限期内摘要到达即
正常替换;决议后冻结,**迟到摘要永不回填**(哈希不变量测试)。N = Flash 动作
序号(与 DataEngine 步序一致)。细节差异:a) 无 job 的历史图(初始观察、摘要
器关闭时的动作图)同样计深并在沿上静默剥除(无占位符)——旧实现是"下一轮即
剥",新时点后移至深度 K;b) 宽限计量单位是"图深度推进"而非回合数(无新图的
空转回合不消耗宽限,上下文也未因图增长);c) 旧 `compress_flash_messages` 原
样保留,作语义对拍参照与回退路径(M2 转写落地后再删)。
3. **配置**:`agent.memory.runtime`(max_concurrency=1/retry_limit=3/
flush_timeout_s=30)与 `agent.memory.transcript`(image_scrub_depth=3/
pending_grace_steps=3/xml_scrub_depth=1)已落 `AgentGlobalConfig.memory`;
显式的旧键 `flash.step_summarizer.retry_limit` 在 before-validator 里播种
`memory.runtime.retry_limit`(新键显式设置时新键胜出,旧键本体不动);
enabled/model/prune_history_xml 三键权威仍在旧块(未迁移)。
`xml_scrub_depth` 已实现为通用深度(>1 时保留更多层 UI 列表),默认 1 与现
行为等价;`prune_history_xml=false` 时冻结也不剥 XML(保旧开关语义)。
4. **FlashRunner 接线**:摘要器以 `memory.runtime` 三值构造;压缩器为 per-run
实例(擦洗账本是运行态);`flush()` 走配置超时。
5. **测试**:+20(`tests/unit/memory/test_step_memory_service.py` 7:零阻塞/有界
重试/别名键控/并发封顶/flush 超时与默认值/重投递清 failed;
`tests/unit/agents/test_flash_scrub_edge.py` 8:成功路径新旧对拍(时点后移
在断言中显式标注)、race a/b/c、冻结哈希不变量、XML 深度 1 等价、
prune 开关、非跟踪消息不触碰;`tests/unit/test_memory_config.py` 5:默认值/
显式值/旧键播种/新键优先/无显式旧键不播种)。存量适配 1 处:
`test_summarizer_keys_actions_independently_of_turn_number` 的
`_step_inputs["tc-1"]` 直取改为 `get_job_payload`/`resolve_key`(step_id 键控
所致)。验证:tests/unit 全量 **1114 通过 3 跳过 0 失败**(M0 基线 1094+3,
只增不减);tests/tools 子集(operator/summarizer/validator/diagnoser)7 通过。
6. **max_turns 未动**(默认 30):擦洗沿使图片上下文稳态有界,更大 max_turns
已安全,放宽属配置决定,留给使用方/后续里程碑。
7. **未完成项**:缓存命中率提升的量化(M1 验收第二条)依赖 M0 尚未录制的真机
基线——计量管线已就绪,待真机长任务前后对比。
**M2 — Pro 转写切换(本方案核心,flag 灰度)** ✅ 代码侧已实施 2026-09-01;✅ 真机 A/B 已完成 2026-09-01(红线全过且大幅改善,含一处阻塞 bug 的最小修复,见 `docs/plans/history-baseline-2026-09-01.md`)
- **前置子任务**:operator.json 模板拆分——今日 SystemMessage 经 `# CURRENT OBSERVATION` 切分后含易变的 plan+history(prompts.py:218-222),必须把静态规则留 S 区、动态内容移出,否则 S 区无从字节稳定;
- `TranscriptLedger` + Operator 双路径并存;转写路径 A/B 对拍旧路径(同任务同设备,姊妹文档 §18 红线:P50/P95 延迟回归 ≤5%、关键路径零摘要等待);转写内时间一律 `T+mm:ss` 起点偏移(冻结后字节稳定),不用 "ago" 措辞;
- SummarizerNode 退化为视觉 lens dispatch(§5 决策已确认);辅助 agent 编译视图接 chunk 前先保持现状。
M2 实施要点(2026-09-01,worktree `zealous-gates-4626b1` 未提交,叠加在 M0+M1 之上;与计划的差异逐条标注):
1. **模板拆分(前置子任务)实现为运行时拆分**:operator.json 本体一字节未动——
唯一易变跨度是字面量 `PLAN_HISTORY_TEMPLATE_SECTION`(`## Current Plan &
Execution History\n{{ plan_and_history }}`),转写路径
`render_transcript_static_system`(prompts.py)把它替换为静态指针段后渲染,
得到整会话字节不变的 S 区(可用性装配 `resolve_operator_prompt_tools` 与
语法开关 `_operator_grammar_flags` 从 TemplatePromptComponent 逐字提取共用,
旧组件行为等价)。硬约束双保险:模板中该字面量"恰出现一次"的结构断言
(缺失即抛错,§9 风险落地)+ 拆分前录制的旧路径渲染 SHA-256 golden
(空计划/哨兵计划两份,`test_operator_transcript.py`)。S 区不再以
`# CURRENT OBSERVATION\n` 结尾——该头衔移入每回合尾部观察消息。
2. **TranscriptLedger**(新 `artemis/memory/transcript.py`):S/F/A 三区实体
+ 每回合 T 尾部由 render 参数传入。A 区 append-only;回合落账两段式
**stage→commit**(step_id 与 Validator 结果在 execution_check/validator 之后
才存在,故本回合消息先 stage,下一回合 build 时带 `step_key` +
`validator_result` 提交)。Validator 结果消息门控
`state.structured_decisions`(planner 拒绝清空、无终端动作为 None,均不落
账),渲染为 `Status: <status>` + format_result_clean 错误/修复细节,带
`T+mm:ss` 偏移(`format_session_offset`,分钟不进位保字节稳定单调)。
观察消息的摘要键走 `id(message)` 侧表(不进消息序列化载荷)。工具配对不
变量:擦洗只改内容块、永不删消息,切分点天然不劈 tool-call/response 对
(单测覆盖)。
3. **擦洗沿复用=泛化 ScrubEdgeCompressor**(未另造 Pro 压缩器):新增三个
构造参数——`summary_key_getter`(无 tool_call_id 的 Pro 观察 HumanMessage
按 step_id 键控查摘要)、`strip_markers`(深度 1 剥除扩展到
`--- Visible UI Elements ---` 与 `--- Task Plan (recited) ---` 计划复诵副本)、
`tail_offset=1`(活尾部在账本外,深度算术与 keep 窗口对齐 Flash 语义)。
默认参数下 Flash 路径字节不变(M1 全部擦洗测试原样通过);宽限/占位符/
冻结不回填纪律在 Pro 消息形态下重跑 M1 用例形态(`test_transcript_ledger.py`)。
`get_step_number` 从 VisualStepSummarizer 上提到 StepMemoryService 基类。
4. **Operator 双路径**(flag `agent.memory.transcript.enabled`,默认 false,
出厂关):关——旧 `_build_prompt` 路径零改动(golden + 存量 20 测试全绿);
开——`_build_prompt_transcript` 产出 S+F+A+尾部,尾部 = `# CURRENT
OBSERVATION [T+mm:ss]` 头 + 计划复诵(新 PlanRecitationPromptComponent,
复诵 task_plan 原文)+ 既有观察/注入/反馈组件原样复用。转写构建异常自动
回退旧路径(灰度安全网)。与计划的差异:a) 过渡接缝取 PromptComponent
机制新增组件而非 `unified_history` 空槽位(改动面更小,任务书允许取小者);
b) 复诵内容是 task_plan checklist 而非 build_plan_and_history 编译串——
历史已由 A 区原始消息承载,整串复诵会大量重复(§3.2 T 区语义即"计划复诵")。
Operator 可见性读集新增 `last_execution_result`(落账需要,documented
decision)。
5. **服务归属收口(M1 差异点 a)**:`ctx.step_memory` / `ctx.transcript_ledger`
成为 ArtemisContext 声明字段(并列而非持有——§6.2 给出的两个选项取后者);
`artemis.memory.ensure_step_memory(ctx)` 为组合根工厂(Operator 转写路径与
SummarizerNode 共用一个实例);FlashRunner 构造后发布到 `ctx.step_memory`
(只发布不消费,兼容存量 mock ctx 测试);`ArtemisContext.__aexit__` 在后台
任务清理前 flush 服务(异常退出 timeout=0 直接取消)。State 不携带任何消息
重物(`test_state_contract.py` 全绿)。
6. **SummarizerNode 切视觉 lens(无条件,非 flag 后)**:不再自调 LLM 生成文
本胶囊;按 step_id 从 DataEngine 取 pre/post 图 bytes(`get_step_image_path`),
动作取步记录 `action_taken`(缺则回退 structured_decisions 解析),dispatch
到 `ctx.step_memory` 视觉 lens(红标叠加、flash_summarizer.md 契约、M0 版本
化 `source="visual_transition"` 写入均沿用)。图缺失→None bytes 纯文本降级,
不抛错。与计划的差异:多动作步 dispatch 首动作为主体、其余进
`action_args["additional_actions"]`(红标叠加只画单动作)。过渡期安全性靠
task_tree.py "无摘要回退 detailed" 语义(未动)。
7. **冷启动构造器(最小版)**:转写空(0 回合、无 F 区)而 DataEngine 已有步
记录时,用 `build_plan_and_history(min_summaries=len(steps),
last_n_detailed=1)` 全量策略渲染冻结块,头部 `[Restored history]` 并注明
"step times relative to the original session start"(该块内的 relative_time
仍是 DataEngine 措辞,冻结一次生成故可接受;A 区从当前步正常追加)。
8. **测试**(只增不减):+22 净增——`tests/unit/memory/test_transcript_ledger.py`
12(S 区哈希稳定/A 区 append-only/T+mm:ss 与禁 ago/深度 1 剥复诵与 UI 列表/
深度 K ready 替换/宽限→占位符→冻结不回填/failed 立即占位/配对不劈/冷启动
块唯一性/双 stage 自动落账/无动作回合不落 Validator 消息);
`tests/unit/agents/test_operator_transcript.py` 7(golden 双哈希/模板单一
切点/S 区静态渲染无历史/两回合四区结构/无动作提交/冷启动 F 区/flag 关
2-message 不变);test_summarizer.py 重写为 4(dispatch 断言与不再写胶囊/
图缺失优雅降级/structured_decisions 回退与 additional_actions/无 step_id
no-op);test_memory_config.py +1(flag 默认关与显式开)。验证:tests/unit
全量 **1136 通过 3 跳过 0 失败**(M1 基线 1114+3,只增不减);tests/tools
子集(operator/summarizer/validator/diagnoser)7 通过。
9. ~~**未完成项(待真机)**~~ **A/B 已完成(2026-09-01,见
`docs/plans/history-baseline-2026-09-01.md`)**:红线全过且大幅改善——全回
合延迟 P50 21.4s→5.0s(−77%)、缓存命中调用 43%→88%(token 29.8%→49.6%)、
输出 token −74%、成功率 6/6 持平、零回退零摘要等待、无签名/格式类错误。
三点必读:① A/B 中发现并最小修复了一个阻塞 bug——`_build_prompt_transcript`
冷启动检查在第 2 回合误命中(上回合只 stage 未 commit,turn_count==0 且
steps 非空),`set_restored_history` 被空账本不变量拒绝→每回合静默回退旧路
径;修复为冷启动条件加 `not ledger.has_staged_turn`(operator.py,已在
worktree,未提交,**需本轨道认领合入并补"第 2 回合 steps 非空"回归测试**)。
② 处理组实际是 M2+M3 叠加(chunking 挂同 flag),纯 M2 归因未拆。③ 实验
模型为 gemini-3.6-flash(3.7 当日 503)。思考签名跨压缩事件的供应商侧验证
仍留 M3 前。灰度顺序照 §10 决策 3:建议修复合入后复测一轮再进默认配置。
**M3 — L2/L3 压缩** ✅ 代码侧已实施 2026-09-01(真机验收待跑)
- 里程碑边界 + 尺寸阈值 + 边界事件三触发;chunk 表 + era 合并;L3 快照;
- 验收:>30 步长任务转写 token 稳态有界;计划改写/回滚后 chunk 身份与可见性不受损(新增回归:重命名走别名链);`[Loop:continuous]` 100+ 步试跑无摘要堆叠。
M3 实施要点(2026-09-01,worktree `zealous-gates-4626b1` 未提交,叠加在 M0+M1+M2
之上;全部行为在 `agent.memory.transcript.enabled` flag 之后;与计划的差异逐条标注):
1. **HistoryChunk 存储**:SQLite 新表 `history_chunks`(chunk_id/session_id/
start·end_step_id/start·end_step_number/source_step_ids/subgoal_hash 标签/
version/status/band1 JSON/band2/band3/rendered_text/created_at)。身份 =
(session_id, 步号范围),行**append-only**:同范围更高 version 追加新行,
`get_history_chunks` 默认取各范围最新版(`all_versions=True` 给审计全轨)。
DataEngine 增 `record_history_chunk`(后台写)/`get_history_chunks`。
**单写者纪律**:全部 DB 写经 HistoryChunkManager 内存镜像(capsule 服务不
直写 DB),版本号无竞争。
2. **三段式 chunk(§3.3 逐字对齐)**:
- **③ 机械账本**(`chunking.build_action_ledger`,不经 LLM):每步一行
`- Step N (T+mm:ss): <format_action_clean> -> <结果短语>`(时间 = 步
timestamp − session_start_time,经 `format_session_offset`,禁 "ago");
结果短语 = format_result_clean 错误/修复细节,success 收敛为 `executed`;
FA 恢复动作(interleaved_events 中 `_exec_*`)以 ` FA: ` 缩进子行经
`format_tool_call_clean` 渲染;injected_instruction 逐字独立行
` User @ Step N: "…"`,**任何层级 never-evict**(含 era ③ 溢出标记与
L3 最小索引,单测覆盖)。为此 `_record_turn` 把 `state.injected_instruction`
盖章进步记录 extra_metadata(execution_check 读集补声明,documented
decision)。差异:多动作步仍一行,首动作 + `(+N more actions)` 后缀。
- **①+② StepCapsuleLens**(`memory/chunking.py` + 提示词
`memory/step_capsule.md`,模型 `agent.memory.chunking.model`):单次调用
产出 JSON(doing/did/effect 三问式——effect 硬性要求含段内 notes 写入的
目标文件与要点,输入里逐步附 save/update/append_note 的 key+content 摘录、
task_plan 写入除外——+ verified_facts/unresolved/failed_paths/
important_entities/entry·exit_state + intervals)。② 区间并集
`validate_interval_coverage` 机械校验(缺口/重叠/越界即该次尝试判负→
StepMemoryService 有界重试→耗尽 failed→chunk 维持 pending,③ 独立可用,
单测覆盖)。中立契约禁词表沿 flash_summarizer.md/summarizer.md 迁入,
`[Loop]` 循环不算异常的判别一并迁入 lens 提示词。**lens 形式化接口收口
(M1 差异点 b)**:`step_memory.StepLens`(`render(key, payload)`)+
StepMemoryService 可选 `lens` 参数与默认 `_attempt` 委托 + `_on_ready`
钩子;差异:VisualStepSummarizer 保留内联 `_attempt`(零迁移),未强迁。
- **异步纪律**:capsule 经 `ChunkCapsuleService`(StepMemoryService 子类,
memory.runtime 三值)后台 dispatch;压缩事件时刻未就绪→冻结 ③ + pending
标记;就绪结果**只在下一次压缩事件**收割(镜像+DB ready 版本写均在收割
时;ctx.__aexit__ 增 chunker.flush 兜底收割落库,无 F 区重渲染)——冻结
块事件间字节稳定有哈希单测。
3. **触发接线**:里程碑唯一事实源 = `_record_turn` 记步后通知
`on_step_stamped(step_id, subgoal_hash)`,chunker 内比较相邻盖章;
`_process_plan_write` 的 `new_top_level_completions` → `queue_boundary_hint`
(未确认边界),下一盖章步 hash 未变即丢弃(棘轮否决回滚的伪切换单测覆盖;
注:执行图在 `_record_turn` 前恒 await planner 验证,盖章天然后于回滚落定)。
尺寸阈值 = 开放段回合数 ≥ `chunking.max_steps` 或段内转写文本 char/4 ≥
`chunking.target_source_tokens`(差异:源 token 估算用账本内该段消息文本,
非"段内摘要"——转写擦洗后消息主体即摘要,作机械代理);软阈值 =
SessionTokenMeter 的 last_prompt_tokens ≥ budget×soft_ratio 时压最旧开放段
(≤max_steps 回合;测试经 `meter_getter` 注入假计量)。**压缩事件在
ledger.render 时执行**(operator 单线程路径;graph 事件只置状态):底层
`_active` 列表保持 append-only(`_active_start` 边界前移,ScrubEdge 索引
恒有效),边界永在完整回合末(不劈 tool-call/response 对,单测覆盖);
加严:闭合段须**整段**滑出 `min_active_steps` 保护窗才压缩——底线只延迟
事件、不切分段(忠于"上一段压成一个 HistoryChunk"),仅尺寸/软阈值可对
开放段出窗部分分片(每片 ≤ max_steps)。
4. **L3 紧急快照**:last_prompt_tokens ≥ budget×hard_ratio 时 F 区渲染为单块
会话快照(overall_goal 取 operator 构造时 initial_goal / plan_state 读
task_plan 原文 / verified·unresolved·failed_paths·entities 结构化字段机械
集合并 / device_app_state 取最新有 capsule chunk 的 exit_state)+ **每
chunk 最小行宽逐步索引**(`build_action_ledger(minimal=True)`:步号+T+时间+
动作短语,user 行保留)。差异:计划"一次 LLM 润色可选"未实现(纯机械合并,
避免摘要的摘要);recent_actions 未入快照(A 区底线本就保留最近原始回合)。
5. **era 合并**:F 区完整 chunk 数超 `chunking.max_chunks`(新键,默认 8)时
最旧溢出段折叠成 era(① 结构化字段集合并非散文再摘要、② 退化为
`- Steps a–b: <里程碑标题|did 首句>` 标题行、③ 逐段 1:1 保留);era 数再超
同一限值时最旧 era 的 ③ 溢出为
`[Era N | Steps a–b: ledger via recall_history]` 标记行(user 注入行仍逐字
保留)。**§10 决策 5 按建议实施,待用户终审**(终审后标记行已升级为时段
段落,见本节末"决策 5 终审收尾")。差异:每次折叠事件生成一个
era(未做跨事件 era 再合并);era 上限复用 max_chunks 同值。
6. **异步检查点回果**:`checkpoints.harvest_run` 在 `_record_verdicts` 后
best-effort 通知 `annotate_from_checkpoint(checkpoint_id, verdicts)`——按
subgoal hash 经 `get_all_subgoal_aliases` 别名链匹配 chunk 标签(重命名
韧性单测),命中即追加 band1.annotations + **version bump + DB 即时追加**,
F 区文本下一压缩事件才重渲染(单测覆盖)。
7. **配置**:新块 `agent.memory.chunking`{max_steps=12, target_source_tokens=
2000, model="gemini-3.7-flash", max_chunks=8};transcript 增
context_budget_tokens=80000 / soft_ratio=0.7 / hard_ratio=0.9 /
min_active_steps=5。真机基线报告(history-baseline-2026-09-01.md)截至实施
时不存在,全部维持计划临时默认,**待定标**。
8. **Flash 侧**:chunker 只在 Pro 转写路径的 TranscriptLedger 上构造
(operator `_ensure_transcript_ledger`,有 DataEngine 才挂);Flash 尚无
ledger,M3 对 Flash 零行为变化。chunker 对里程碑事件是可选输入,尺寸/软硬
阈值纯凭 ledger 回合元数据触发——Flash 未来接 ledger 时自动呈现"只有尺寸
阈值触发"的任务书形态。
9. **测试**:+29 净增——`tests/unit/memory/test_history_chunking.py` 25
(③ 结构 golden 与 FA 子行/注入逐字/最小行宽可寻址、② 覆盖校验与缺口
重生成与有界重试降级 pending、三段顺序 golden(对照 §3.3)、真切换整段
成 chunk、伪切换回滚丢弃、尺寸两式、软/硬假计量注入、min_active 底线、
F/A 边界不劈对、冻结字节稳定与就绪不即时回填、回果 version bump 与下次
事件重渲染、重命名别名链、era 合并 ③ 保留、era 溢出标记与 never-evict、
全层级步可寻址不变量、lens 接口);
`tests/unit/data_engine/test_history_chunk_storage.py` 4(roundtrip/版本
append-only 最新胜出/范围排序/会话隔离)。验证:tests/unit 全量
**1165 通过 3 跳过 0 失败**(M2 基线 1136+3,只增不减);tests/tools 子集
(operator/summarizer/validator/diagnoser)7 通过。
10. **未完成项(待真机/后续)**:验收三条真机项(>30 步 token 稳态、
`[Loop:continuous]` 100+ 步无堆叠试跑、capsule 质量盲评);辅助 agent
编译视图接 chunk digest(§10 决策 4 建议 M3 末——本次任务书未含,留 M4
与 recall 一起收口);阈值定标依赖真机基线报告。
**M4 — recall + 策略表收拢** ✅ 代码侧已实施 2026-09-01(阈值定标待真机)
- `recall_history` 工具 + 屏幕相似度提示;ContextPolicy 8 调用点逐点对拍收拢;配置收敛与旧键兼容别名。
M4 实施要点(2026-09-01,worktree `zealous-gates-4626b1` 未提交,叠加在 M0+M1+M2+M3
之上;与计划的差异逐条标注):
1. **recall_history 工具**(新 `artemis/tools/history_recall.py`,规格照姊妹文档
§10):签名 `(query, step_range=None, include_details=False,
include_images=False, max_results=5)`。第一期实现为 **Python 扫描而非
SQLite FTS5**(差异:现库无任何 FTS5/索引,且 `get_agent_friendly_steps`
已一次性给出全部检索面;FTS 留待量级需要时加)——检索面:步摘要/精确动作/
Validator 结果/Operator 思考/FA trace 与注入指令(步记录)、XML/OCR 文本
(`storage.get_image`,**扫描封顶最近 150 步**)、notes 文件(`utils/notes`)、
history_chunks(band2/band3/rendered_text)。包/Activity 检索经 XML blob
文本命中(差异标注:`record_step` 的 `foreground_app` 形参历史上从未被任何
调用方写入,包名不单独持久化)。边界全部落地:结果数钳到
`recall.max_results`;响应 char/4 估算超 `max_text_tokens` 即截断并提示收窄;
**每条结果带步号**(notes 锚定到最近写入步,无写入记录则"as of Step N");
include_details 每字段限幅;include_images 仅回捞**单步**真实存在的图
(data-URL 块,形态镜像 ObservationPromptComponent;文件不存在给文字说明,
永不多于 1 步)。**step_range 请求总是附带该范围的 `build_action_ledger`
全宽行**——era ③ 溢出标记行 `[Era N | Steps a–b: ledger via recall_history]`
由此获得真实入口。空 query 且无 step_range 拒绝执行(回用法提示)。
2. **注册(仅 Pro,Flash 不动)**:graph.py `operator_specialized_wrappers`
追加 wrapper;`DEFERRING_TOOLS` 加 recall_history(pre-decision 语义:与
Turn-Ending 动作同回合即拒绝动作);提示词 `_PRE_DECISION_MEMORY_TOOLS`
加名 + operator.json 在 TOOL USE PROTOCOL 第 1 类下加
`[% if "recall_history" in available_tools %]` gated 指导段(六触发:似曾
相识/旧精确值/两步无进展/第三次同一动作/摘要与观察冲突/用户要求回早前
状态;明示禁投机召回;标记行=显式入口)。可用性 = `ctx.data_engine` 存在
且 `memory.recall.enabled`——M2 的 SHA-256 golden 用 `data_engine=None`
的 ctx 录制,故 **golden 无需重录、原样通过**;受影响的是全集字面断言
(见要点 6)。manifest:`BACKEND_INDEPENDENT_TOOLS` 加 recall_history
(分类完整性测试强制)。
3. **屏幕相似度本地提示**(零模型调用、零历史图入 prompt):新
`artemis/utils/image_hash.py` 纯 PIL 64 位 dHash(JPEG draft 快解码,
哈希对重编码稳健故无需像素组件的 q75 对称重编码技巧);`record_step`
**同步**盖章 `pre/post_image_dhash` 入 extra_metadata(同步是为了避免与
后台写线程的并发字典突变;draft 解码使成本毫秒级;post==pre 时 post 哈希
取 pre 哈希——"无独立后图证据"但后屏即前屏)。新
`HistoricalStateHintPromptComponent` 插在 ScreenshotSimilarity 之后(legacy
与 transcript 两条组件清单都插):**近 3 步任一命中即整体静默**(该 regime
属像素级"卡在同屏"组件,两者永不同时触发);更老步扫描封顶 500 步
(含已 chunk 化段——`get_agent_friendly_steps` 全量),距离≤阈值取最近的
最小距离步,注入方案原文句式提示。
4. **ContextPolicy 策略表**(新 `artemis/memory/context_policy.py`):8 个
生产调用点(operator/operator_cold_start/failure_analyzer/planner/
outputter/history_analyzer/diagnoser/committee)收拢为每 agent 一条
`ContextPolicy` 声明,字段 1:1 映射 build_plan_and_history kwargs
(`uncompressed`=min_summaries=len(steps) 惯用法、`whitelist`=运行时传
keep_subgoal_hashes 两个便捷位);`build_history_for(agent, ...)` 为唯一
入口,operator 的 last_n_detailed 构造器旋钮保留为运行时覆盖。**逐点
golden 对拍**:`test_context_policy.py` 参数化 8 例逐字节断言 + "全表必有
golden"覆盖断言。`agent.memory.policies` 按 agent 覆盖字段(未知字段告警
忽略)。差异:第 9 个调用点 `mcp_server/tools/inspect_trace.py` 属离线
审计渲染而非 agent 编译视图,未入表(保持直调)。
5. **编译视图接 chunk(§10 决策 4 收口)**:`build_plan_and_history` 增可选
`chunks` 入参——形状无关 dict(步号范围 + 预渲染文本),chunk 块紧随
`--- Execution History ---` 按序输出,范围内步行被替换(detailed 窗口与
最新步永不吞);**chunks 为 None/空时输出逐字节不变**(golden 锁定)。
视图渲染在策略层:`full`(outputter/history_analyzer)= 持久化
rendered_text 原样(含 ③ 全宽行);`digest`(diagnoser/committee)=
rendered_text 在 "③ Step action ledger" 标题处截断 + recall 标记行
`③ Step action ledger: available via recall_history (Steps a–b)`。chunk
仅当调用点传 engine + transcript flag 开 + 有持久化行时进入;engine 仅从
四个辅助 agent 调用点传入,operator/planner/FA/冷启动永不接(冷启动策略
`chunk_view=None` 显式声明)。**行为增益(实施标注)**:diagnoser/
committee 的 strict/whitelist 悬崖在长任务中首次由段外 digest 块补偿——
chunk 块总是全量渲染,不受步级可见性策略过滤。差异:digest 从持久化
rendered_text 截断而非从 band1/band2 重渲染(零重复渲染逻辑;pending 态
chunk 自然退化为 pending 说明 + 标记行)。
6. **配置**:`agent.memory.recall`{enabled=true, max_results=5,
max_text_tokens=2000, max_image_steps=1}、transcript 增
`similarity_hint=true` / `similarity_max_distance=8`、`memory.policies={}`。
真机基线报告(history-baseline-2026-09-01.md)截至实施时仍不存在,相似度
距离阈值与 recall 预算默认值**维持临时、待定标**。旧键兼容别名本阶段无
新增需求(recall/similarity 均为全新键)。
7. **测试**(只增不减):+45 净增——`tests/unit/tools/test_history_recall.py`
13(关键词命中与步号不变量、step_range 过滤、范围回捞全宽行(era 入口)、
结果数钳制、token 截断、notes 检索与步锚定、chunk 行检索、图仅单步仅真实
文件、配置封零、缺图降级、空参拒绝、无引擎降级、可用性三态);
`tests/unit/utils/test_image_hash.py` 5(稳定性/JPEG 重编码小距离/异屏大
距离/垃圾输入 best-effort/距离边界);
`tests/unit/agents/test_historical_state_hint.py` 8(命中注入、近 3 步
静默、无命中/关开关/步数不足静默、平距取最近、无哈希跳过、record_step
盖章含 post==pre 镜像);`tests/unit/memory/test_context_policy.py` 16
(8 点 golden 参数化、全表覆盖、配置覆盖与未知 agent、flag 关字节不变、
full/digest 渲染、chunk_view=None 不接、最新步不被吞);
test_memory_config.py +2(recall/similarity/policies 默认与覆盖)。存量
断言同步 4 处:test_prompt_assembly 全集枚举字面与 removed-tool 参数化加
recall_history、test_operator_prompts 记忆工具枚举字面、
test_history_analyzer patch 目标改 build_history_for、action_manifest 分类
集。验证:tests/unit 全量 **1210 通过 3 跳过 0 失败**(M3 基线 1165+3,
只增不减);tests/tools 子集(operator/summarizer/validator/diagnoser)
7 通过。
8. **未完成项(待真机/后续)**:相似度距离阈值与 recall 预算定标依赖真机
基线报告;include_images 的图片块(ToolMessage 多模态内容)在 Gemini 工具
响应管线的端到端表现待真机验证;Flash 注册 recall 留待其 ledger 接 chunk
之后(方案既定);FTS5 索引留待检索量级需要时引入。
**硬化轮 — A/B 实测问题收口** ✅ 已实施 2026-09-01(worktree `zealous-gates-4626b1` 未提交,叠加在 M0-M4 之上;任务来源 = `history-baseline-2026-09-01.md` §5/§6 的移交项)
1. **M2 冷启动 bug 认领合入 + 回归测试**:A/B 会话的最小修复(`_build_prompt_transcript`
冷启动条件加 `not ledger.has_staged_turn`,operator.py,含注释)经核实形态正确
完整,本轮正式认领。`test_operator_transcript.py` 的两回合用例数据构造改为
真实形态——第 1 回合无步记录、第 2 回合 steps 非空且上一回合 staged 未
commit(原用例 `data_engine=None` 使 steps 恒空,正因此没抓到);负向验证
已做:临时移除修复条件该用例即失败。
2. **chunk ①② 段头全 pending 根因(已定论)+ 修复**:worktree
traces/data_engine.db 实证——dispatch 一环正常(21 个 chunk 全部 submit),
断环在 LLM:**capsule 尝试 41 次全部 "503 Service Unavailable"**,5 个 job
走到有界重试耗尽进 failed,其余 16 个卡在 `max_concurrency=1` 串行队列没轮
到尝试就随会话结束;"下一次压缩事件收割 + flush 兜底收割落库"链路本身无缺
陷,只是全程无就绪结果可收(21 行 DB 记录恒 version=1 pending 与此吻合)。
根因 = `chunking.model` 默认 gemini-3.7-flash 当日全天 503,而 capsule lens
走 `get_google_llm` 原始旁路,没有网关的分类重试/降级模型(同日 A/B 主模型
手工换 3.6、step lens 3.5-flash-lite 均健康,唯 capsule 绑死单一端点)。
修复:`StepCapsuleLens` 增降级模型链——`HistoryChunkManager` 从 llm 配置
summarizer 角色的 fallback(继承全局 default.fallback,现值
gemini-3.6-flash)解析同 provider(google)且异于主模型的降级模型,render
经 `services.llm.with_fallback` 分类切换(503/超时类才降级,bad request 不
掩盖进弱模型)。"短会话等不到下一次压缩事件"的假设不成立——flush 收割落库
M3 已实现,本轮补单测锁定(迟到就绪 → flush 收割 → DB ready v2 落库、冻结
文本不重渲染)。单测另覆盖:降级成功(主 503 → fallback 出 capsule)、无降
级时 503 耗尽 failed 且 chunk 维持 pending、降级模型解析三态(google-only、
不等于主模型)。
3. **视觉 lens 单图自适应(§5 修订版落地)**:新增
`flash_summarizer_single.md` 单图变体提示词——只描述决策帧内容与动作落点
(红标),**无 AFTER ACTION 段**,明示"缺后图仅表示无独立后验证据,严禁描
述/推测/合成动作后状态(转换叙事由相邻步摘要链接)",禁判定词表原样沿用;
`_attempt` 按"前后双图 → 原转换模板、其余(Pro 恒缺后图、任一图缺失、纯文
本)→ 单图变体"选择,单图块标签改为 DECISION FRAME(仅后图存在时亦有中性
标签)。Pro 无动作后抓屏的刻意安全网未动(不补抓、不用步 N+1 前图拼合)。
**回显校验兜底**:`degenerate_summary_reason`——输出含 "--- [" 段标记或以
"---" 开头、<15 字符、>1500 字符即判该次尝试失败,走既有有界重试,耗尽进
failed;双图模板补一句"输出永不含 --- 分隔符"降低无谓重试。单测覆盖单图/
双图模板选择与块标签、回显判负后重试恢复、echo/过短/过长三形态耗尽 failed。
4. **视觉 lens 计量接入(M0 遗留收口)**:`record_llm_usage`/
`SessionTokenMeter.record` 增 `update_last_prompt` 开关;视觉 lens 与
capsule lens 的原始旁路调用成功后各记一条 `llm_usage` trace(source
`lens:visual_transition:<model>` / `lens:step_capsule:<model>`),一律
`update_last_prompt=False`——lens 小 prompt 不得覆写 chunker 软/硬阈值消费
的 `last_prompt_tokens` 上下文基数;网关包装模型(RobustChatModelWrapper)
已在出口自计量,旁路计量以 isinstance 判别避免重复。M0 实施要点第 3 条
"raw 旁路仅测试使用"的表述至此与现状一致(旁路仍在,但已计量)。
5. **定标落地**:`memory.runtime.max_concurrency` 默认 1→2(依据基线报告
§6:串行队列使摘要就绪 P90 长尾超擦洗窗口 K=3+宽限;lens 是独立轻模型调
用,并发 2 风险低),Field description 已标注依据与出处;
`similarity_max_distance=8` 维持待定标(基线未产生相似屏配对数据)。
6. **就绪门控交换(§3.3 修订落地;任务中途经同伴会话转达用户拍板补入本轮)**:
HistoryChunkManager 事件流重构为"关段 ≠ 交换"——触发器(里程碑/尺寸/软阈
值)只**关段**:建 chunk 镜像、落 v1 pending DB 行(recall/辅助视图可见)、
dispatch 段头生成,原始回合原样留在转写 A 区(`_awaiting` 队列,lossless
pending 从图片层贯彻到段层);**交换(freeze_turns + 渲染 chunk 块)发生在
段头就绪后的首次渲染**,且只交换就绪的连续前缀(交换消耗最旧未压回合,老
段 pending 时后继段一并保留原文)。降级梯照 §3.3:生成失败(有界重试耗尽)
→ 原文保留 + 重投(lens 每次尝试自带 fallback 模型,见第 2 项;重投节奏限
触发/软硬压力渲染,不逐渲染,避免重引入无界 LLM 重试);软阈值压力下仍未
就绪 → 依旧保留原文(正确性优先于 token);**仅硬阈值**强制交换全部已关段
(pending 一并入 L3 快照,③ 最小索引存活)。checker post-hoc 标注独立于段
头(awaiting 态即刻 DB version bump),冻结文本仍只在交换事件重渲染(事件
间字节稳定)。无步记录的关段(chunk 缺失)永不常规交换,仅硬阈值随队清
出。CHUNK_PENDING_NOTE 增 recall_history 指引。A/B 的"21 chunk 段头全
pending"在门控下退化为"尚未压缩"的安全形态。单测:里程碑用例改为"关段不
交换 → 就绪后下次渲染交换"全流程、冻结区事件间字节稳定重写为门控形态、
软压力不交换、失败重投且原文保留、硬阈值强制入 L3 等新用例;触发器/era/
L3/寻址性用例改用 auto-resolve capsule 桩保持原有触发语义断言。
7. **段头笔记关联升级为机械校验(同伴会话转达用户拍板补入)**:
`parse_capsule` 在 ② 区间覆盖校验之后增加笔记覆盖校验——payload 中段内
note_writes 的去重 key 必须以子串出现在 band① 任一文本字段(doing/did/
effect/entry/exit/四个结构化列表),缺任何一个该次尝试判负返回 None 走既
有有界重试,耗尽维持 pending(配合门控:不交换、原文保留);
step_capsule.md 明示该硬约束(machine-checked)。单测:双笔记段漏一 key
判负、补全通过、无笔记段不受影响。
验证:tests/unit 全量 **1224 通过 3 跳过 0 失败**(M4 基线 1210+3,只增不
减,净增 14);tests/tools 子集(operator/summarizer/validator/diagnoser)
7 通过。灰度前置 ①(§10 决策 3 / 基线报告 §7-3)至此满足;前置 ②(修复后
复测一轮 A/B)仍待真机,且复测轮应同时验收门控形态(段头就绪率、交换延
迟、A 区稳态尺寸)。
**M5 — 缓存深化(可选)** ✅ 终轮已实施 2026-09-01(worktree `zealous-gates-4626b1` 未提交,叠加在 M0-M4+硬化轮之上)
- S 区显式 CachedContent(explorer 先例外推);`short_term_memory` 移除(姊妹文档 §3.3,转写落地后其职责已被真多轮取代);供应商 stateful API 评估。
M5 实施要点(2026-09-01;与计划的差异逐条标注):
1. **默认启用(用户拍板)**:`agent.memory.transcript.enabled` 默认 false→true
(`config/agent.py` 默认值 + `config/artemis.jsonc` 新增完整 `agent.memory`
块同步——jsonc 此前根本没有 memory 块;L2/L3 chunking 挂同一 flag 随同默认
开)。**旧路径与转写构建异常自动回退逻辑全部保留不删——`enabled: false`
即回滚开关**,置 false 逐字节回旧 2-message 行为(Field description 与
jsonc 注释均已标注,含 A/B 依据摘要)。测试纪律:凡"flag 关"语义的既有
测试改为显式传 false(`test_operator.py` 全部 21 处构造加
`transcript_config=LEGACY_TRANSCRIPT`;`test_memory_config.py` 默认断言改
true、显式 false 改为"回滚开关"用例),"flag 开"路径成为默认被测面。涟漪
仅一处:`test_outputter` 的 Mock engine 需补 `get_history_chunks=[]`(辅助
视图默认开始咨询 chunk 存储,空 chunk 时输出逐字节不变的 M4 语义未变)。
2. **short_term_memory 全链移除(姊妹文档 §3.3 落地)**:按 visibility 附录 B
台账纪律先核销全部消费者再删——State 字段(state.py)、operator 提取/写块
(operator.py 返回 dict 与 `<short_term_memory>` 正则提取)、
`ShortTermMemoryPromptComponent`(prompts.py 类 + operator.py 两条组件清单
引用)、visibility manifest operator 读/写集、operator.json 的
Short-Term Memory 指令段(含示例块,模板不再教模型产出该标签)。
sdk `_get_graph_state` 经核实已是 `State.initial()` 单源(Phase 0/1 收拢),
字段删除即覆盖。**两处防御性清洗有意保留**:`task_tree.py` 渲染侧的
`<short_term_memory>` 标签剥除与 showcase_ui markdown-parser 的标签过滤——
历史 DB 中旧会话的思考原文仍含该标签,姊妹文档 §3.3 原文要求
"safely ignoring it in older responses"。
3. **C 清尾(M0-M4 标注遗留小项逐项处置)**:
| # | 遗留项 | 处置 |
|---|---|---|
| C1 | 旧 `compress_flash_messages`(M1 标注"M2 落地后再删") | **已删**。语义对拍测试改造:`test_flash_scrub_edge.py` 两处 legacy 参照改为对 ScrubEdge 产物的直接字面断言;`test_flash_step_summarizer.py` 两例转为 ScrubEdge(`image_scrub_depth=1` 复现 legacy 时点,ordinal 回退键控保留覆盖),三例存档删除(pending 回退/迟到回填为 ScrubEdge 明令禁止的旧语义,前缀保留已由 scrub_edge 用例覆盖) |
| C2 | `record_step` 的 `foreground_app` 形参从未落库(M4 发现) | **已做**。显式形参优先落 `extra_metadata["foreground_app"]`;形参缺席时从 ui_tree 的 package 属性纯字符串推导(最频繁非 systemui 包,`_derive_foreground_app`,零设备调用零同步开销);recall `_step_haystack` 检索面接入——包名查询不再仅依赖封顶 150 步的 XML blob 扫描。测试 +4 |
| C3 | click_sequence/swipe 多点动作语义充实(M0 差异) | **已做**。`_target_semantics` 扩展:click_sequence 逐点命中测试(`points_semantics` 列表)、swipe 打 start/end 两点(`start/end_semantics`),**首点语义提升为主 `target_*` 标签**;全程 best-effort 永不抛。测试 +2(原"swipe 跳过充实"断言按新行为改写) |
| C4 | era 上限复用 max_chunks 同值(M3 差异) | **已做**。新键 `chunking.max_eras`(默认 None=随 max_chunks,保 M3 等值行为);chunker `_fold_eras` 的 era 溢出改用独立上限。测试 +2(宽松 era 上限不溢出/紧上限提前溢出、config 默认与覆盖) |
| C5 | `similarity_max_distance=8` 待定标(M4) | **已定标 8→5**。worktree traces/data_engine.db 有 460 步 dHash 盖章(16 会话):同屏重拍(同 SHA 相邻/wait 步/同 SHA 远距重访)距离集中 ≤4;相邻步分布双峰、谷在 5-6;跨应用会话对(T1 Settings × T2 时钟,保证异屏,n=11,770)≤5 仅 0.14%(阈 8 时 0.24%,假阳翻倍)。取谷值 5:同屏召回不损、异屏假阳减半。定标证据写入 Field description |
| C6 | M2 golden(模板拆分前 SHA-256) | **已按新现状重录一次**:operator.json 移除 Short-Term Memory 指令段(本轮 B 项唯一有意模板变更)使旧哈希失效;重录 EMPTY/SENTINEL 双哈希并在测试注释注明重录原因与时间。golden 的"任何其他漂移即 flag-off 路径不再逐字节"约束继续有效 |
4. **D1 — S 区显式 CachedContent:评估后决定不实现**。依据:A/B 实测隐式缓存
已达 88% 调用命中/49.6% token 命中(基线 43%/29.8%),S 区字节稳定 + 转写
append-only 正是隐式缓存的理想形态,边际收益只剩"稳态间隔太长导致隐式条目
过期"的场景;而显式 CachedContent 要求最低 4,096 token 起步、按 token-hour
计存储费、需要显式生命周期管理(创建/续期/删除),并把 S 区绑定到单一模型
版本(fallback 切模型即缓存失效)。当前 Operator 步间隔 P50 8.8s 远短于隐式
条目寿命,付费显式缓存买不到增量命中。**重启条件**(写给未来):若出现
①步间隔常态 >5min 的长轮询任务(`[Loop:continuous]` 大 Interval)实测隐式
命中率显著回落,或 ②S 区膨胀(如工具 schema 大幅增长)使单次 re-prefill
成本可观,则按 explorer.py:1709 先例实现,config 开关默认关。
5. **D2 — Gemini stateful/Interactions API 评估备忘**:见新增 §11(只评估不
实现)。
6. **归档**:状态行更新为已实施;§10 决策逐条标注(决策 5 维持"已按建议实施、
待用户终审");两份姊妹文档头部注记补最终状态指针。
验证:tests/unit 全量 **1229 通过 3 跳过 0 失败**(硬化轮基线 1224+3,只增
不减,净增 5:+8 新增 −3 存档删除);tests/tools 子集(operator/summarizer/
validator/diagnoser)7 通过。
**最终残留(2026-09-01 终轮验收后更新)**:~~recall include_images 真机端到
端;最终验收轮真机三件套(>30 步 token 稳态、`[Loop:continuous]` 100+ 步、
capsule 盲评——此轮同时验收就绪门控形态与默认开后的复测 A/B)~~ **已全部
完成并通过**(2026-09-01 最终真机验收轮,五项全过:4 对同模型 A/B 回退 0/
就绪率 60/60/延迟−60%/缓存 40%→86%;112 步 Loop 无堆叠、token 有界 33% 预
算、注入逐字保全;48 chunk 盲评禁词违规 1 例且语义无害;recall 图片块 3 次
Gemini 调用全 success;相似度提示正例+静默负例均验证——详见
`docs/plans/history-acceptance-2026-09-01.md`)。仍留:git 提交/PR(用户决
定,全部改动仍在 worktree `zealous-gates-4626b1` 未提交);Flash 接
TranscriptLedger/chunk(文档既定后续项,Flash 侧 transcript.enabled 无效果、
零行为变化);两项不阻塞优化候选([Loop] 里程碑内相似度提示静默、
gemini-3.7-flash 供应商稳定性观察,验收报告 §6-a/e)。
**决策 5 终审收尾** ✅ 已实施 2026-09-01(用户终审 2026-09-01 定案,worktree
`zealous-gates-4626b1` 未提交;叠加在 M0–M5 之上):
1. **形态变化**:era 溢出的极限层从 M3 的"纯标记行"
`[Era N | Steps a–b: ledger via recall_history]` 升级为 §3.3 终审定案的
**时段段落**——
`[Era N | Steps a–b | T+hh:mm → T+hh:mm] <一段话概括>` + 缩进 recall 指引行
`(Step-level ledger via recall_history for steps a–b)` + user 注入行逐字保留
(never-evict 不随丢步豁免)。起止时间取该 era 首 chunk `start_offset` 与末
chunk `end_offset`(既有 T+ 会话偏移格式),是视频对齐的粗粒度外键。
2. **段落机械拼装,零新增 LLM 调用**(`chunking.render_era_period_paragraph`):
doing 取各 chunk ① 首个非空;did 逐 chunk 取首句串联(band1 缺失/pending 的
chunk 回退用 milestone_label,无标签再退 `steps a–b`);effect 中笔记引用
(`notes/<key>` 路径与 `*.md` 文件名正则)合并去重;verified_facts 集合并后
取前 4 条。全部 chunk 无 band1 时段落退化为"里程碑标签列表 + 步范围"——
**永不为空**(新增单测覆盖两级回退)。
3. **代码位置**:`artemis/memory/chunking.py`(`RECALL_MARKER_TEMPLATE` 更名
`RECALL_GUIDANCE_TEMPLATE` 并改为缩进指引行;`render_era_block` recall_only
分支重写;新增 `render_era_period_paragraph`/`_first_sentence`);recall 端
无解析耦合(`recall_history` 的 step_range 入口本就按步号范围回捞,与标记
行字面无关),仅同步了 `artemis/tools/history_recall.py` 文档字符串与
`artemis/agents/operator/operator.json` 提示词中对入口行形态的描述。
4. **不变量修订落地**(§8 同步):"全层级步可寻址"单测改为 L2 chunk/era 合并/
L3 快照层逐步可寻址 + **极限段落层时段可寻址**(正则断言 `Steps a–b` 与
`T+..→T+..` 在场)。era 溢出用例改断言新形态(时间范围在场、段落非空、
doing/笔记引用/verified 进段落、指引行与 user 行仍在)。
5. **验证**:tests/unit 全量 **1231 通过 3 跳过 0 失败**(M5 终轮基线 1229+3,
只增不减,净增 2:时段段落逐 chunk 回退 + 全缺失退化两用例)。
每阶段独立可回滚;M2 起所有新行为在 flag 后面,关 flag 即回现状(M5 后 flag 出厂开,关 flag 语义不变)。
---
## 8. 验收红线(继承并加严)
> **终轮判定(2026-09-01):六条红线全部 PASS**("速度/质量"两条因样本量为方向性判定,"对拍"引用 M5 单测 golden)——逐条依据见 `docs/plans/history-acceptance-2026-09-01.md` §7。
- **不损伤**:压缩产物一律可经 step 引用回捞原文;chunk/快照生成失败时源摘要与 detailed 渲染回退可用(现 `task_tree.py:920` 语义保留为最后防线)。
- **步骨架不变量**:L2 chunk、era 合并、L3 快照层级后,转写中步 1:1 可寻址——每步的步号与相对时间可直接读出,无合并、无丢步、无重排;视频分析模块按步号/时间戳的对齐在压缩前后逐步一致(新增回归测试)。**唯一例外:极限时段段落层**(决策 5 终审)——允许丢步,但段落必须带步号范围与起止时间(时段可寻址),用户注入行仍逐字保留,步级细节 recall 可回捞。
- **速度**:关键路径零摘要等待、零新增同步调用;延迟回归 ≤5%(30 对任务、置信区间)。
- **缓存**:M1/M2 各自报告命中率提升(这是本方案区别于现状的核心收益,必须量化)。
- **质量**:重复动作率、历史致回滚率不升;跨计划改写的关键事实保留率上升;摘要/Validator 冲突趋零。
- **对拍**:辅助 agent 编译视图收拢前后逐字节一致;Flash M1 语义等价对拍。
---
## 9. 风险与回退
| 风险 | 缓解 |
|---|---|
| 转写路径引入回归(最大风险面) | 双路径并存 + flag;A/B 对拍先行;State/图结构不动 |
| Gemini 隐式缓存命中不及预期(条目短寿命、前缀下限) | M0 先测真实命中率;不及预期则 M5 显式缓存提前;方案收益不全押缓存(真多轮/签名连续性独立成立) |
| chunk 质量差导致远期决策劣化 | 结构化字段合并而非散文再摘要;recall 兜底;盲评(姊妹文档 §17 三方对比法沿用) |
| flash-lite 摘要质量上限 | lens 模型可配;闲时用 3.7 重摘(姊妹文档 §8.3 隔离模式保留) |
| 无界任务 chunk 无限增长 | era 二级合并 + 预算封顶 + L3 快照 |
| 转写崩溃丢失 | DataEngine 为事实源,冷启动构造器重建 F 区(A 区丢失退化为冷启动,可接受) |
| 裸坐标动作账本无语义("click 82,32 看不出在点什么") | M0 记录时命中测试充实 `target_text`(§6.1);`target_label_source` 区分指名与推断 |
| 异步检查点结果在段 chunk 化之后才回果,① 的 verified/unresolved 过时 | chunk 版本化 + checkpoint harvest 对命中步范围的 chunk 追加 post-hoc 标注并 bump version;F 区文本在下一次压缩事件时才重渲染(不做即时深突变) |
| 一步内 FA 恢复动作视频轴有帧、账本无行 | ③ 缩进子行机械渲染 `_exec_*`(数据已在 interleaved_events) |
| 段内用户注入指令被压缩叙事吞掉 | ③ 逐字保全行 + never-evict(§3.3 账本细则) |
| 回合内工具循环(≤20 次迭代)在两次计量之间撑爆上下文 | 16K 单工具信封 + 回合内累计估算越硬阈值时强制决策(姊妹文档 §13 语义接入 L3 触发检查) |
| 计划写入被否决回滚造成伪里程碑切换 | 触发事实源锁定为已执行步骤盖章变化;计划事件只入队待确认(§3.3) |
| chunk 化删除携带思考签名的旧 AIMessage 影响后续轮 | 压缩点永远在完整回合边界(不劈 tool-call/response 对);M2 真机验证项 |
| **今日 Pro 的 SystemMessage 本身含易变历史**——模板在 `# CURRENT OBSERVATION` 处切分,part 0(含 `## Current Plan & Execution History`)进系统消息(prompts.py:218-222);不拆模板则 S 区无从稳定,缓存收益归零 | M2 前置子任务:operator.json 模板拆分——静态规则/计划语法留 S 区,plan+history 移出系统消息(姊妹文档 §12 同一要求,在此升为 M2 硬依赖) |
| 转写内时间戳字节不稳定:现 `get_relative_time` 是 "x min ago" 措辞,冻结后即变陈旧谎言,重算则破字节稳定 | ③ 账本一律用会话起点偏移 `T+mm:ss`(冻结后永真且字节稳定);"ago" 措辞仅限辅助 agent 的每次重编译视图 |
| 极端规模下 ③ 与有限预算的数学冲突:`[Loop:continuous]` 跑到数千步时,即便最小行宽的逐步账本也会耗尽预算 | 开放决策 §10:最老 era 的 ③ 溢出到 recall-only(转写留 `Steps 1–500: ledger via recall_history` 标记行),或预算随任务放宽——需用户定溢出规则 |
| 模型路由 fallback 切换后 prompt_tokens 分词口径不一致 | 计量当近似值用,软/硬阈值留 10%+ 余量;切换事件记入计量日志 |
---
## 10. 开放决策(待用户拍板)
1. ~~**步级单 lens 修订**(§5)~~ **已拍板(2026-08-31):采纳**。步级仅保留 `VisualTransitionLens`(双 profile 共用),Pro 步级胶囊上移为 chunk 级 `StepCapsuleLens`。原否决备选(双摘要并存、`update_step_summary` 加 lens 维度、成本 ×2)作废。
2. ~~**默认值定标**~~ **已决(2026-09-01)**:基线报告 §6 定标——`context_budget_tokens=80K`/软硬阈值/K=3/min_active=5/chunk≤12 全部维持(实测远未触及,触发行为验收留 `[Loop:continuous]` 真机轮);`max_concurrency` 1→2(硬化轮);`similarity_max_distance` 8→5(M5 依 460 步 dHash 分布定标,见 §7 M5 要点 3-C5)。
3. ~~**M2 灰度顺序**~~ **已决(2026-09-01)**:A/B 红线全过且大幅改善后,用户拍板 M5 将 `transcript.enabled` 提为默认开;`enabled:false` 保留为逐字节回滚开关。基线报告 §7-3 的两个前置中 ①(冷启动修复认领+回归测试)已在硬化轮满足,②(修复后复测轮)并入最终验收真机轮。
4. ~~**辅助 agent 视图升级时点**~~ **已决**:M4 收口(编译视图接 chunk digest,full/digest 双渲染,见 §7 M4 要点 5)。
5. **极端规模的 ③ 溢出规则**:~~最老 era 的 ③ 溢出到 recall-only 标记行~~ **已终审定案(用户 2026-09-01,修订原实施)**:极限层允许丢步——最老 era 压缩为"时段段落"(一段话概括 + **步号范围与起止时间必须在场** + recall 指引 + user 注入行逐字保留),段落机械拼装自 era 各 chunk ① 字段。见 §3.3 极限层小节;M3 原"纯标记行"实施按此升级(终审收尾轮落地)。
---
## 11. 供应商 stateful API 评估备忘(M5,只评估不实现)
对象:Gemini **Interactions API**(2026 年起 Google 推荐的新接口面,与本仓现用的
`generateContent` 并行提供)。其与本架构直接相关的三个特性(官方文档核实于
2026-09-01):
- **服务端会话状态**:`store: true`(默认开)+ 后续请求带
`previous_interaction_id` 即可让服务端续接完整对话历史,客户端无需重发消息;
付费层交互对象保留 55 天(免费层 1 天),可配置 7/14/28/55 天窗口,可显式删除。
- **思考签名服务端化**:stateful 模式下 thought signature(Gemini 3 起的加密思考
连续性凭据)由服务端随会话状态自动管理,客户端完全无需回传/维护——本方案
§3.2 的"思维签名政策"(回合内强制、A 区顺带、压缩即弃)在该模式下整体消失为
非问题。
- **显式缓存不支持**:Interactions API 只有隐式缓存;显式 `CachedContent` 仅
`generateContent` 提供。若未来重启 D1(显式缓存),则与迁移 Interactions API
互斥,需二选一。
### 11.1 与本架构的相容性评估
| 维度 | 现架构(M0-M5 转写) | Interactions API stateful | 评估 |
|---|---|---|---|
| 历史表示 | 客户端 TranscriptLedger 四区,字节级自主控制 | 服务端黑盒累积,客户端只发新尾部 | **根本冲突**:擦洗沿(深度 K 图→摘要替换)、L2 chunk 化、L3 快照全部依赖对历史消息的**受控深突变**——服务端状态是 append-only 黑盒,无法就地替换第 i 条消息的图片块,也无法把 12 步折叠成一个 chunk 块。要保留分层压缩就必须在压缩事件时**放弃续接、整段重建新会话**(丢 `previous_interaction_id` 换新 id),届时"免重发历史"的收益只在压缩事件之间存在 |
| 缓存 | 隐式缓存实测 88% 调用命中(A/B) | 隐式缓存由服务端在续接时自动优化 | 收益近似打平:本架构的 S 区稳定 + append-only 已把隐式缓存吃到接近上限,服务端续接省的是**上行带宽与序列化**,不是缓存未命中 |
| 思考签名 | A 区字节不动即免费有效,压缩即弃(§3.2) | 服务端全托管 | Interactions 略优,但现政策实测零签名错误(A/B 红线),痛点不存在 |
| 可逆性/审计 | DataEngine 全量在案,recall 可回捞 | 服务端 55 天留存,之后自动删除 | 现架构必须保留 DataEngine 事实源不动摇;服务端状态只能作缓存层,不可作事实源 |
| 生态 | LangChain `langchain-google-genai`(generateContent 系)+ 本仓网关(分类重试/fallback 模型链) | 需直连新 API 面;fallback 切模型时服务端会话是否可跨模型续接未验证 | 迁移面 = LLM 网关全重写 + fallback 语义重新设计,改动半径大 |
| 隐私/合规 | 请求即用即弃(模型侧) | 默认 `store: true`,设备操作史(截图、输入文本)在 Google 侧留存最长 55 天 | 移动 UI 自动化的截图流含敏感界面,默认服务端留存需要显式合规评估;`store: false` 则失去 stateful 收益,自相矛盾 |
### 11.2 结论
**不迁移,不部分采用(2026-09-01)。** 三条硬理由:① 分层压缩(本方案的核心
价值:token 稳态有界 + 步骨架不变量)与服务端黑盒状态根本不相容,保它就得在每
次压缩事件重建会话,收益归零;② 现架构在缓存命中与签名连续性上的实测表现已接
近 Interactions 能给的上限(88% 命中、零签名错误),痛点侧无增量;③ 截图流默
认服务端留存 55 天是移动自动化场景新增的合规面。
**重估触发条件**(写给未来):① Google 宣布 `generateContent` 弃用时间表(届时
迁移变为强制,方案是"压缩事件即会话重建"的混合形态,本备忘 §11.1 第一行即设计
起点);② Interactions API 补齐显式缓存与服务端历史编辑(可控修剪/替换)能力;
③ 出现"无压缩短会话"型新 profile(如 <15 步的 Flash 任务),可单独试点
stateful 模式免去消息重建。