update: memoryシステムの"Phase"表記を撤廃
This commit is contained in:
+11
-10
@@ -96,14 +96,14 @@ pub struct MemoryConfig {
|
||||
/// Ignored when the request omits `query`. `None` ⇒ tool default (3).
|
||||
#[serde(default)]
|
||||
pub query_excerpt_lines: Option<usize>,
|
||||
/// Optional model for the Phase 1 (extract) worker. When `None`,
|
||||
/// Optional model for the extract worker. When `None`,
|
||||
/// the main pod model is cloned via `clone_boxed()`. Lightweight
|
||||
/// reasoning-capable models (Haiku / 4o-mini / Flash class) are
|
||||
/// recommended.
|
||||
#[serde(default)]
|
||||
pub extract_model: Option<ModelManifest>,
|
||||
/// Cumulative input-token threshold (since the last extract pointer)
|
||||
/// that triggers a Phase 1 extract. `None` disables Phase 1
|
||||
/// that triggers an extract run. `None` disables the extract trigger
|
||||
/// entirely; memory tools and resident injection still work, only
|
||||
/// the auto-extract trigger is dormant.
|
||||
#[serde(default)]
|
||||
@@ -119,20 +119,21 @@ pub struct MemoryConfig {
|
||||
/// [`defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS`] when unset.
|
||||
#[serde(default)]
|
||||
pub extract_worker_max_turns: Option<u32>,
|
||||
/// Optional model for the Phase 2 (consolidation) worker. When
|
||||
/// Optional model for the consolidation worker. When
|
||||
/// `None`, the main pod model is cloned via `clone_boxed()`.
|
||||
/// Reasoning-class models are recommended.
|
||||
#[serde(default)]
|
||||
pub consolidation_model: Option<ModelManifest>,
|
||||
/// Phase 2 trigger: file-count threshold of `_staging/`. Phase 2
|
||||
/// fires when the staging directory has at least this many entries.
|
||||
/// Either threshold reaching its limit fires Phase 2 (logical OR).
|
||||
/// `None` for both thresholds ⇒ Phase 2 disabled.
|
||||
/// Consolidation trigger: file-count threshold of `_staging/`. The
|
||||
/// consolidation run fires when the staging directory has at least
|
||||
/// this many entries. Either threshold reaching its limit fires
|
||||
/// consolidation (logical OR). `None` for both thresholds ⇒
|
||||
/// consolidation disabled.
|
||||
#[serde(default)]
|
||||
pub consolidation_threshold_files: Option<usize>,
|
||||
/// Phase 2 trigger: byte-size threshold across all `_staging/`
|
||||
/// entries. Either threshold reaching its limit fires Phase 2.
|
||||
/// `None` for both thresholds ⇒ Phase 2 disabled.
|
||||
/// Consolidation trigger: byte-size threshold across all `_staging/`
|
||||
/// entries. Either threshold reaching its limit fires consolidation.
|
||||
/// `None` for both thresholds ⇒ consolidation disabled.
|
||||
#[serde(default)]
|
||||
pub consolidation_threshold_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Phase 2 sub-Worker への最初のユーザー入力を組み立てる。
|
||||
//! consolidation sub-Worker への最初のユーザー入力を組み立てる。
|
||||
//!
|
||||
//! Phase 1 (`extract::build_extract_input`) と同じ方針で、固定 schema の
|
||||
//! extract (`extract::build_extract_input`) と同じ方針で、固定 schema の
|
||||
//! markdown セクション列にしてサブWorker に渡す。`docs/plan/memory.md`
|
||||
//! §Phase 2 入力 / §整理材料 の項目に従い:
|
||||
//! §Consolidation 入力 / §整理材料 の項目に従い:
|
||||
//!
|
||||
//! 1. consumed staging エントリ全文(`source` 込み)
|
||||
//! 2. 既存 `memory/*` 全文(summary / decisions / requests)
|
||||
@@ -10,7 +10,7 @@
|
||||
//! 4. 整理材料(Linter Warn ベース、メトリクス未完なら明示 invoke 頻度なし)
|
||||
//!
|
||||
//! 既存 `knowledge/*` 本文は埋めず、agent に `KnowledgeQuery` 経由で引かせる
|
||||
//! 設計(`docs/plan/memory.md` §retrieval 経路 / §Phase 2 の Knowledge アクセス)。
|
||||
//! 設計(`docs/plan/memory.md` §retrieval 経路 / §Consolidation の Knowledge アクセス)。
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::workspace::{RecordKind, WorkspaceLayout};
|
||||
|
||||
/// Knowledge 化候補レポート。`tickets/memory-usage-metrics.md` の成果物が
|
||||
/// 出るまでは空で渡す前提(`docs/plan/memory.md` §Knowledge 化候補レポート)。
|
||||
/// 空入力時、統合 phase は新規 Knowledge を作らず decisions / requests /
|
||||
/// 空入力時、統合 step は新規 Knowledge を作らず decisions / requests /
|
||||
/// summary / 既存 Knowledge update に留まる。
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct KnowledgeCandidateReport {
|
||||
@@ -45,7 +45,7 @@ impl KnowledgeCandidateReport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 2 sub-Worker の最初の user 入力。
|
||||
/// consolidation sub-Worker の最初の user 入力。
|
||||
pub fn build_consolidate_input(
|
||||
layout: &WorkspaceLayout,
|
||||
staging: &[StagingEntry],
|
||||
@@ -54,9 +54,9 @@ pub fn build_consolidate_input(
|
||||
) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(
|
||||
"Phase 2 consolidation input. Run the consolidation phase first \
|
||||
"consolidation input. Run the integration step first \
|
||||
(fold the staging activity logs into memory and knowledge), then the \
|
||||
tidy phase (clean up existing records). Use the memory tools for \
|
||||
tidy step (clean up existing records). Use the memory tools for \
|
||||
every write — direct file writes are denied by the pod scope.\n\n",
|
||||
);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! `_staging/.consolidation.lock` による Phase 2 占有ファイル。
|
||||
//! `_staging/.consolidation.lock` による consolidation 占有ファイル。
|
||||
//!
|
||||
//! `docs/plan/memory.md` §並走防止 に従い:
|
||||
//!
|
||||
//! - ファイルが存在し、記録された Pod が動作している間、その Pod が排他占有
|
||||
//! - クラッシュで残った stale lock は、所有者 PID が死んでいれば次回 spawn
|
||||
//! 時に上書き取得できる
|
||||
//! - cleanup は consumed ID の staging エントリのみ削除し、実行中に Phase 1
|
||||
//! - cleanup は consumed ID の staging エントリのみ削除し、実行中に extract
|
||||
//! が追加した分は残す
|
||||
//!
|
||||
//! 占有判定は Linux/macOS の `kill(pid, 0)` 経由で行う(`ESRCH` で死亡判定)。
|
||||
@@ -29,7 +29,7 @@ pub struct LockRecord {
|
||||
pub pid: u32,
|
||||
pub pod_name: String,
|
||||
pub started_at: DateTime<Utc>,
|
||||
/// この Phase 2 run が起動時スナップショットで確定した consumed staging
|
||||
/// この consolidation run が起動時スナップショットで確定した consumed staging
|
||||
/// entry の UUIDv7 列。完了時はこの列のみ削除し、追加分は残す。
|
||||
pub consumed_ids: Vec<Uuid>,
|
||||
}
|
||||
@@ -38,7 +38,7 @@ pub struct LockRecord {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LockError {
|
||||
/// 占有ファイルが既にあり、所有者 PID が生きているのでスキップ。
|
||||
#[error("Phase 2 lock held by live pid {pid} (pod {pod_name:?})")]
|
||||
#[error("consolidation lock held by live pid {pid} (pod {pod_name:?})")]
|
||||
InUse { pid: u32, pod_name: String },
|
||||
#[error("io error at {}: {source}", .path.display())]
|
||||
Io {
|
||||
@@ -59,7 +59,7 @@ impl LockError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 2 が走っている間 RAII で持つ占有ハンドル。`Drop` では何もしない —
|
||||
/// consolidation が走っている間 RAII で持つ占有ハンドル。`Drop` では何もしない —
|
||||
/// 完了時の cleanup は consumed ID 列削除と一緒に行う必要があるため、明示
|
||||
/// 解放 [`StagingLock::release_with_cleanup`] を使う。明示解放しないまま
|
||||
/// drop された場合は占有ファイルがそのまま残り、次回 spawn 時に PID が
|
||||
@@ -105,10 +105,10 @@ impl StagingLock {
|
||||
tracing::warn!(
|
||||
stale_pid = existing.pid,
|
||||
stale_pod = %existing.pod_name,
|
||||
"Phase 2 stale lock detected, taking over"
|
||||
"consolidation stale lock detected, taking over"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(path = %path.display(), "Phase 2 lock unparseable, treating as stale");
|
||||
tracing::warn!(path = %path.display(), "consolidation lock unparseable, treating as stale");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ impl StagingLock {
|
||||
self.unlink_lock_only();
|
||||
}
|
||||
|
||||
/// 占有ファイルだけ削除し、staging エントリには触らない。Phase 2
|
||||
/// 占有ファイルだけ削除し、staging エントリには触らない。consolidation
|
||||
/// sub-Worker が途中で失敗した場合に使う: 入力 staging を残したまま
|
||||
/// 次回再評価で再処理させる(`docs/plan/memory.md` §並走防止 の
|
||||
/// 「重複作成は同一 slug update に自然収束」運用)。
|
||||
@@ -160,7 +160,7 @@ impl StagingLock {
|
||||
tracing::warn!(
|
||||
path = %self.path.display(),
|
||||
error = %e,
|
||||
"failed to remove Phase 2 lock"
|
||||
"failed to remove consolidation lock"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -193,7 +193,7 @@ fn pid_is_alive(pid: u32) -> bool {
|
||||
#[cfg(not(unix))]
|
||||
fn pid_is_alive(_pid: u32) -> bool {
|
||||
// Unsupported platforms: assume the lock is live so we never overwrite
|
||||
// someone else's claim. Phase 2 will skip and try again next post-run.
|
||||
// someone else's claim. consolidation will skip and try again next post-run.
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Phase 2: 統合 + 整理。
|
||||
//! consolidation: 統合 + 整理。
|
||||
//!
|
||||
//! Phase 1 が staging に残した活動ログを `memory/*` / `knowledge/*` に
|
||||
//! extract が staging に残した活動ログを `memory/*` / `knowledge/*` に
|
||||
//! 統合し、続けて既存 record を `outdated | superseded | unused | noisy`
|
||||
//! の観点で整理する disposable Worker を、Pod 側が組み立てるための
|
||||
//! ヘルパー群を提供する。Pod は次の手順で sub-Worker を構築する:
|
||||
@@ -15,7 +15,7 @@
|
||||
//! (`PodPrompt::MemoryConsolidationSystem`) で管理される。Knowledge 化候補
|
||||
//! レポートと使用頻度メトリクスは別チケットで供給される想定。本モジュール
|
||||
//! 時点では空入力として扱い、prompt 側の説明だけ残しておく
|
||||
//! (`docs/plan/memory.md` §Phase 2 / 整理材料)。
|
||||
//! (`docs/plan/memory.md` §Consolidation / 整理材料)。
|
||||
|
||||
mod input;
|
||||
mod lock;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! `_staging/*.json` を列挙して [`StagingRecord`] に展開する読み込みヘルパー。
|
||||
//!
|
||||
//! Phase 2 起動時のスナップショット(consumed ID list 確定)と、整理 phase
|
||||
//! consolidation 起動時のスナップショット(consumed ID list 確定)と、整理 step
|
||||
//! が終わった後の cleanup の双方で使う。`.consolidation.lock` のような
|
||||
//! 占有ファイルは UUIDv7 として parse できないので自然に除外される。
|
||||
//!
|
||||
//! [`StagingRecord`] のスキーマは Phase 1 が書き出す側 (`crate::extract`)
|
||||
//! [`StagingRecord`] のスキーマは extract が書き出す側 (`crate::extract`)
|
||||
//! と単一の真実源 — ここでは読み出す側だけを担当する。
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -29,7 +29,7 @@ pub struct StagingEntry {
|
||||
/// `<staging_dir>/*.json` を読んで UUIDv7 順に並べた [`StagingEntry`]
|
||||
/// 配列を返す。staging_dir が存在しなければ空配列。読めないファイルや
|
||||
/// JSON parse 失敗は `tracing::warn!` してスキップ(壊れた個別ファイルが
|
||||
/// Phase 2 全体を止めないように)。
|
||||
/// consolidation 全体を止めないように)。
|
||||
pub fn list_staging_entries(layout: &WorkspaceLayout) -> Vec<StagingEntry> {
|
||||
let dir = layout.staging_dir();
|
||||
let entries = match std::fs::read_dir(&dir) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! 整理 phase が prompt 入力に乗せる「整理材料」スキャナ。
|
||||
//! 整理 step が prompt 入力に乗せる「整理材料」スキャナ。
|
||||
//!
|
||||
//! `docs/plan/memory.md` §整理(GC 相当)の扱い と
|
||||
//! `tickets/memory-phase2-consolidation.md` の整理材料リストに従い、
|
||||
//! `tickets/memory-consolidation.md` の整理材料リストに従い、
|
||||
//! メトリクス未完の現状で機械的に拾えるヒントだけを集める:
|
||||
//!
|
||||
//! - `replaced` chain: `status: replaced` の Decision とその `replaced_by`
|
||||
@@ -21,13 +21,13 @@ use crate::workspace::{RecordKind, WorkspaceLayout};
|
||||
|
||||
/// `sources` overflow を flag する閾値。`linter::warnings::SOURCES_OVERFLOW_THRESHOLD`
|
||||
/// と同値(10)を踏襲する。Linter Warn で sources 過多が検出されるラインと
|
||||
/// 整理 phase で勧告するラインを揃える狙い。
|
||||
/// 整理 step で勧告するラインを揃える狙い。
|
||||
pub const SOURCES_OVERFLOW_THRESHOLD: usize = 10;
|
||||
/// 類似 slug クラスタリングの距離。`linter::warnings::SIMILAR_SLUG_DISTANCE`
|
||||
/// と同値。
|
||||
pub const SIMILAR_SLUG_DISTANCE: usize = 2;
|
||||
|
||||
/// 整理 phase 用の機械集計ヒント。空フィールドは「対象なし」を意味する。
|
||||
/// 整理 step 用の機械集計ヒント。空フィールドは「対象なし」を意味する。
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TidyHints {
|
||||
/// `status: replaced` で残っている Decision の slug → `replaced_by` map。
|
||||
@@ -179,7 +179,7 @@ fn parse_yaml<F: serde::de::DeserializeOwned>(content: &str) -> Option<F> {
|
||||
|
||||
/// Connected-component clustering over the `levenshtein <= SIMILAR_SLUG_DISTANCE`
|
||||
/// graph among same-kind slugs. Returns each cluster of size >= 2 (singleton
|
||||
/// clusters are not interesting for the integration phase). Returns `None`
|
||||
/// clusters are not interesting for the integration step). Returns `None`
|
||||
/// when there are no clusters at all.
|
||||
fn cluster_similar(slugs: &[&str], kind: RecordKind) -> Option<Vec<SimilarSlugCluster>> {
|
||||
if slugs.len() < 2 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Phase 1 sub-Worker への入力テキスト組み立て。
|
||||
//! extract sub-Worker への入力テキスト組み立て。
|
||||
//!
|
||||
//! `crates/pod/src/pod.rs::build_summary_prompt` と同じ方針で
|
||||
//! Item 列を flat な行に落とす(reasoning は省く、tool call は名前のみ、
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
use llm_worker::Item;
|
||||
|
||||
/// 与えられた `items` を Phase 1 sub-Worker の最初の user 入力に整形する。
|
||||
/// 与えられた `items` を extract sub-Worker の最初の user 入力に整形する。
|
||||
pub fn build_extract_input(items: &[Item]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Phase 1: 活動抽出。
|
||||
//! extract: 活動抽出。
|
||||
//!
|
||||
//! 通常 Pod の post-run hook で発火する disposable Worker と、その
|
||||
//! 出力を `<workspace>/.insomnia/memory/_staging/<id>.json` に書き出す
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Phase 1 抽出の出力 schema。
|
||||
//! extract 抽出の出力 schema。
|
||||
//!
|
||||
//! LLM は [`ExtractedPayload`] そのもの(source 抜き)を返し、Pod 側
|
||||
//! ラッパーが [`StagingRecord`] に組み立てて staging へ書き出す。
|
||||
//! source は機械付与する契約 (`docs/plan/memory.md` §Phase 1)。
|
||||
//! source は機械付与する契約 (`docs/plan/memory.md` §Extract)。
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::EXTRACT_DOMAIN;
|
||||
|
||||
/// Phase 1 完了境界の永続化 payload。session log の Extension entry
|
||||
/// extract 完了境界の永続化 payload。session log の Extension entry
|
||||
/// として 1 回ずつ書かれ、最新の 1 件が現行 pointer として有効になる。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ExtractPointerPayload {
|
||||
@@ -21,7 +21,7 @@ pub struct ExtractPointerPayload {
|
||||
pub staging_id: String,
|
||||
}
|
||||
|
||||
/// `RestoredState.extensions` から最新の Phase 1 pointer を取り出す。
|
||||
/// `RestoredState.extensions` から最新の extract pointer を取り出す。
|
||||
/// 未抽出セッションでは `None`。
|
||||
pub fn fold_pointer(extensions: &[(String, serde_json::Value)]) -> Option<ExtractPointerPayload> {
|
||||
extensions
|
||||
|
||||
@@ -35,7 +35,7 @@ pub enum StagingError {
|
||||
///
|
||||
/// 戻り値は割り当てられた staging file の (id, path)。`payload` が
|
||||
/// 完全に空の場合は呼び出し側が事前に `is_empty()` で skip 推奨だが、
|
||||
/// この関数は空でも正規に書き出す(仕様 §Phase 1 で空配列許容と
|
||||
/// この関数は空でも正規に書き出す(仕様 §Extract で空配列許容と
|
||||
/// 明記されており、書く / 書かないの判断は呼び出し側に委ねる)。
|
||||
pub fn write_staging(
|
||||
layout: &WorkspaceLayout,
|
||||
|
||||
@@ -17,7 +17,7 @@ Pass an object with `decisions`, `discussions`, `attempts`, and `requests` array
|
||||
Call this exactly once and end the turn. Do not include `source`, session metadata, or free-form prose — \
|
||||
the wrapper attaches provenance mechanically.";
|
||||
|
||||
/// Phase 1 sub-Worker の出力受け口。`ExtractedPayload` 1 件をホストする。
|
||||
/// extract sub-Worker の出力受け口。`ExtractedPayload` 1 件をホストする。
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ExtractWorkerContext {
|
||||
payload: Mutex<Option<ExtractedPayload>>,
|
||||
|
||||
@@ -183,7 +183,7 @@ impl WorkspaceLayout {
|
||||
}));
|
||||
}
|
||||
if first == STAGING_DIR {
|
||||
// Linter opts out of `_staging/`; Phase 1 handles its schema.
|
||||
// Linter opts out of `_staging/`; extract handles its schema.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
/// 任意の history index 時点でのプロンプト全長推定。
|
||||
///
|
||||
/// `total_tokens()` と同じ accounting を任意位置で評価する版。
|
||||
/// memory phase 1 trigger が
|
||||
/// memory extract trigger が
|
||||
/// `total_tokens_at(now) - total_tokens_at(pointer)` で
|
||||
/// pointer 以降に増えたプロンプト長を測るのに使う。
|
||||
pub fn total_tokens_at(&self, history_len: usize) -> TokenEstimate {
|
||||
|
||||
+34
-34
@@ -157,32 +157,32 @@ pub struct Pod<C: LlmClient, St: Store> {
|
||||
/// When true (default), the system-prompt assembler walks
|
||||
/// `<workspace>/knowledge/*` and appends a `## Resident knowledge`
|
||||
/// section listing records with `model_invokation: true`.
|
||||
/// Phase 2 (consolidation) workers set this to false so the
|
||||
/// consolidation workers set this to false so the
|
||||
/// agentic worker pulls knowledge through the search tools instead.
|
||||
inject_resident_knowledge: bool,
|
||||
/// Latest runtime scope snapshot queued by dynamic scope changes.
|
||||
/// Drained into the session log before the next turn result is
|
||||
/// persisted, so resume never silently reclaims delegated writes.
|
||||
pending_scope_snapshot: Arc<Mutex<Option<PodScopeSnapshot>>>,
|
||||
/// Phase 1 (memory.extract) reentry guard. `true` while an extract
|
||||
/// extract (memory.extract) reentry guard. `true` while an extract
|
||||
/// worker is running; subsequent triggers are skipped per spec
|
||||
/// (`docs/plan/memory.md` §Phase 1 並走防止). `Arc<AtomicBool>` so
|
||||
/// (`docs/plan/memory.md` §Extract 並走防止). `Arc<AtomicBool>` so
|
||||
/// the flag survives across `try_post_run_extract` calls without a
|
||||
/// `&mut self` race.
|
||||
extract_in_flight: Arc<AtomicBool>,
|
||||
/// Phase 2 (memory.consolidation) in-process reentry guard. The
|
||||
/// consolidation (memory.consolidation) in-process reentry guard. The
|
||||
/// staging-side `StagingLock` already provides cross-process
|
||||
/// exclusion, but this AtomicBool keeps a careless concurrent caller
|
||||
/// inside the same Pod from racing on the staging snapshot.
|
||||
consolidation_in_flight: Arc<AtomicBool>,
|
||||
/// Last completed Phase 1 boundary. `None` means no extract has
|
||||
/// Last completed extract boundary. `None` means no extract has
|
||||
/// run yet on this session — next extract starts from entry 0.
|
||||
/// Restored from `RestoredState.extensions` on `restore`, updated
|
||||
/// after each successful extract via `save_extension`.
|
||||
extract_pointer: Arc<Mutex<Option<memory::ExtractPointerPayload>>>,
|
||||
/// Phase 1/2 memory job running outside the controller method loop.
|
||||
/// extract/consolidation memory job running outside the controller method loop.
|
||||
/// The task owns the extract/consolidate worker execution and is joined
|
||||
/// at shutdown. A single slot is enough: Phase 1/2 implementations loop
|
||||
/// at shutdown. A single slot is enough: extract/consolidation implementations loop
|
||||
/// until thresholds fall below their trigger points, and concurrent
|
||||
/// triggers are coalesced by skipping when this handle is still active.
|
||||
memory_task: Option<JoinHandle<()>>,
|
||||
@@ -255,7 +255,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
||||
pub fn spawn_post_run_memory_jobs(&mut self) {
|
||||
// Drop a finished prior handle so we can spawn a fresh task.
|
||||
// If the prior task is still running, coalesce by skipping —
|
||||
// Phase 1/2 implementations re-evaluate thresholds on completion.
|
||||
// extract/consolidation implementations re-evaluate thresholds on completion.
|
||||
self.cleanup_finished_memory_task();
|
||||
if self.memory_task.is_some() {
|
||||
return;
|
||||
@@ -350,7 +350,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
///
|
||||
/// Default `true`: when memory is enabled in the manifest, the
|
||||
/// assembler walks `<workspace>/knowledge/*` and lists records with
|
||||
/// `model_invokation: true`. Phase 2 (consolidation) workers and
|
||||
/// `model_invokation: true`. consolidation workers and
|
||||
/// other agentic memory paths set this to `false` so the worker
|
||||
/// pulls knowledge through the search tools instead of riding on
|
||||
/// the resident system-prompt budget. Idempotent if called multiple
|
||||
@@ -507,7 +507,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Snapshot of the Phase 1 (memory.extract) boundary pointer.
|
||||
/// Snapshot of the extract (memory.extract) boundary pointer.
|
||||
///
|
||||
/// `None` means no extract has run yet on the current session — the
|
||||
/// next extract will start from entry 0. Updated by
|
||||
@@ -531,7 +531,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Test/diagnostic handle to the Phase 2 in-flight guard. Production
|
||||
/// Test/diagnostic handle to the consolidation in-flight guard. Production
|
||||
/// callers do not need this; tests use it to assert that the reentry
|
||||
/// guard skips an in-progress consolidation without losing data.
|
||||
#[doc(hidden)]
|
||||
@@ -846,7 +846,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
}
|
||||
}
|
||||
// Resident-injection collection: only when memory is enabled in
|
||||
// the manifest AND this Pod opts in (Phase 2 workers opt out).
|
||||
// the manifest AND this Pod opts in (consolidation workers opt out).
|
||||
// Owned `Vec` lives for the duration of `render` below; the
|
||||
// context borrows a slice into it.
|
||||
let resident: Vec<memory::ResidentKnowledgeEntry> = if self.inject_resident_knowledge {
|
||||
@@ -1733,13 +1733,13 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
.expect("usage_history poisoned")
|
||||
.clear();
|
||||
self.persist_scope_snapshot().await?;
|
||||
// Reset Phase 1 pointer alongside usage_history: the compacted
|
||||
// Reset extract pointer alongside usage_history: the compacted
|
||||
// session has a fresh log with no `LogEntry::Extension` entries
|
||||
// yet, so a cold restore here would set extract_pointer to None
|
||||
// via fold_pointer. The in-memory pointer must match — otherwise
|
||||
// `tokens_added_since(old_history_len)` would treat the new
|
||||
// (shorter) history as if it had already been processed, and
|
||||
// Phase 1 would stop firing for the rest of the process's
|
||||
// extract would stop firing for the rest of the process's
|
||||
// lifetime.
|
||||
*self
|
||||
.extract_pointer
|
||||
@@ -1764,7 +1764,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
Ok(worker.client().clone_boxed())
|
||||
}
|
||||
|
||||
/// Build the LlmClient for the Phase 1 (memory.extract) Worker.
|
||||
/// Build the LlmClient for the extract (memory.extract) Worker.
|
||||
///
|
||||
/// Uses `memory.extract_model` from manifest if set, otherwise clones
|
||||
/// the main client.
|
||||
@@ -1780,7 +1780,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
Ok(worker.client().clone_boxed())
|
||||
}
|
||||
|
||||
/// pointer 以降に増えたプロンプト全長の推定。Phase 1 trigger が
|
||||
/// pointer 以降に増えたプロンプト全長の推定。extract trigger が
|
||||
/// 閾値判定に使う。
|
||||
///
|
||||
/// `total_tokens_at(now) - total_tokens_at(pointer)` の差分で、
|
||||
@@ -1799,14 +1799,14 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
total_now.saturating_sub(total_at_pointer)
|
||||
}
|
||||
|
||||
/// Phase 1 (memory.extract) post-run trigger.
|
||||
/// extract (memory.extract) post-run trigger.
|
||||
///
|
||||
/// Called by the Controller before spawning the background memory task so
|
||||
/// the extract worker sees a stable session-log entry range while compact
|
||||
/// is deferred until the next turn starts. Best-effort: failures are
|
||||
/// logged but not propagated.
|
||||
///
|
||||
/// Behaviour follows `docs/plan/memory.md` §Phase 1 並走防止:
|
||||
/// Behaviour follows `docs/plan/memory.md` §Extract 並走防止:
|
||||
/// in-flight 中の trigger は skip し、完了時点で閾値再評価する
|
||||
/// (the loop below). Pending state is not retained — the
|
||||
/// re-evaluation happens naturally because the in-memory pointer
|
||||
@@ -1845,11 +1845,11 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Phase 1 extract failed");
|
||||
tracing::warn!(error = %e, "extract failed");
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Pod,
|
||||
format!("memory Phase 1 extract failed: {e}"),
|
||||
format!("memory extract failed: {e}"),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1955,7 +1955,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
|
||||
let payload = ctx.take_payload().unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
"Phase 1 extract worker did not call write_extracted; \
|
||||
"extract worker did not call write_extracted; \
|
||||
advancing pointer with empty payload"
|
||||
);
|
||||
extract::ExtractedPayload::default()
|
||||
@@ -2002,7 +2002,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
Ok(ExtractDecision::Completed)
|
||||
}
|
||||
|
||||
/// Build the LlmClient for the Phase 2 (memory.consolidation) Worker.
|
||||
/// Build the LlmClient for the consolidation (memory.consolidation) Worker.
|
||||
///
|
||||
/// Uses `memory.consolidation_model` from manifest if set, otherwise
|
||||
/// clones the main client. Mirrors [`build_extractor_client`].
|
||||
@@ -2018,13 +2018,13 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
Ok(worker.client().clone_boxed())
|
||||
}
|
||||
|
||||
/// Phase 2 (memory.consolidation) trigger.
|
||||
/// consolidation (memory.consolidation) trigger.
|
||||
///
|
||||
/// Intended to run from a background memory task after Phase 1 may have
|
||||
/// Intended to run from a background memory task after extract may have
|
||||
/// added staging entries. Compact is deferred until the next turn starts,
|
||||
/// so consolidation no longer blocks the controller's post-run path.
|
||||
///
|
||||
/// Behaviour follows `docs/plan/memory.md` §Phase 2 / §並走防止:
|
||||
/// Behaviour follows `docs/plan/memory.md` §Consolidation / §並走防止:
|
||||
/// the staging-side `StagingLock` enforces cross-process exclusion;
|
||||
/// `consolidation_in_flight` keeps in-process callers honest. On
|
||||
/// success, the lock is released *with* consumed-id cleanup; on
|
||||
@@ -2035,9 +2035,9 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
return Ok(());
|
||||
};
|
||||
// `Some(0)` collapses to `None` — staging count / bytes always
|
||||
// satisfies `>= 0`, which would fire Phase 2 on every post-run.
|
||||
// satisfies `>= 0`, which would fire consolidation on every post-run.
|
||||
// Treating zero as disabled lines up with `extract_threshold` and
|
||||
// matches the "no threshold ⇒ Phase 2 off" invariant in the
|
||||
// matches the "no threshold ⇒ consolidation off" invariant in the
|
||||
// ticket's §Trigger.
|
||||
let files_threshold = memory_cfg.consolidation_threshold_files.filter(|n| *n > 0);
|
||||
let bytes_threshold = memory_cfg.consolidation_threshold_bytes.filter(|n| *n > 0);
|
||||
@@ -2062,11 +2062,11 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
Ok(ConsolidateDecision::Skipped) => return Ok(()),
|
||||
Ok(ConsolidateDecision::Completed) => continue,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Phase 2 consolidation failed");
|
||||
tracing::warn!(error = %e, "consolidation failed");
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Pod,
|
||||
format!("memory Phase 2 consolidation failed: {e}"),
|
||||
format!("memory consolidation failed: {e}"),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -2135,7 +2135,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
// directly under the workspace via WorkspaceLayout. Resident
|
||||
// knowledge injection (`Pod::set_resident_knowledge_injection`) is
|
||||
// a Pod-level concern; this disposable Worker is built without it
|
||||
// by construction, in keeping with `docs/plan/memory.md` §Phase 2
|
||||
// by construction, in keeping with `docs/plan/memory.md` §Consolidation
|
||||
// のKnowledgeアクセス (agent pulls knowledge through the search
|
||||
// tool instead of via system-prompt residency).
|
||||
let query_cfg = memory::tool::QueryConfig::from(memory_cfg);
|
||||
@@ -2167,7 +2167,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of a single Phase 1 extract iteration. Internal to
|
||||
/// Outcome of a single extract iteration. Internal to
|
||||
/// `try_post_run_extract` / `run_extract_once`.
|
||||
enum ExtractDecision {
|
||||
/// Threshold not reached, or no items to extract.
|
||||
@@ -2208,7 +2208,7 @@ impl llm_worker::interceptor::Interceptor for MemoryExtractWorkerInterceptor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of a single Phase 2 consolidation iteration. Internal to
|
||||
/// Outcome of a single consolidation iteration. Internal to
|
||||
/// `try_post_run_consolidate` / `run_consolidate_once`.
|
||||
enum ConsolidateDecision {
|
||||
/// Either threshold not met, no staging, or another Pod holds the lock.
|
||||
@@ -2722,10 +2722,10 @@ pub enum PodError {
|
||||
#[error(transparent)]
|
||||
PromptCatalog(#[from] CatalogError),
|
||||
|
||||
#[error("memory Phase 1 staging write failed: {0}")]
|
||||
#[error("memory extract staging write failed: {0}")]
|
||||
ExtractStaging(#[source] memory::extract::StagingError),
|
||||
|
||||
#[error("memory Phase 2 lock acquisition failed: {0}")]
|
||||
#[error("memory consolidation lock acquisition failed: {0}")]
|
||||
ConsolidationLock(#[source] memory::consolidate::LockError),
|
||||
|
||||
#[error("workflow load failed: {0}")]
|
||||
|
||||
@@ -61,9 +61,9 @@ const INTERNAL_TOML: &str = include_str!("../../../../resources/prompts/internal
|
||||
pub enum PodPrompt {
|
||||
/// System prompt of the compaction (summary) Worker.
|
||||
CompactSystem,
|
||||
/// System prompt of the memory Phase 1 (extract) Worker.
|
||||
/// System prompt of the memory extract Worker.
|
||||
MemoryExtractSystem,
|
||||
/// System prompt of the memory Phase 2 (consolidation + tidy) Worker.
|
||||
/// System prompt of the memory consolidation (integration + tidy) Worker.
|
||||
MemoryConsolidationSystem,
|
||||
/// Wrapper around an incoming `Method::Notify` message injected into
|
||||
/// the next LLM request context as a transient system message.
|
||||
|
||||
@@ -151,12 +151,12 @@ pub struct SystemPromptContext<'a> {
|
||||
pub agents_md: Option<String>,
|
||||
/// Resident-injection candidates from `<workspace>/knowledge/*` whose
|
||||
/// frontmatter has `model_invokation: true`. `None` disables the
|
||||
/// section entirely (memory disabled, or a Phase 2 worker that opts
|
||||
/// section entirely (memory disabled, or a consolidation worker that opts
|
||||
/// out); `Some(&[])` also yields no section.
|
||||
pub resident_knowledge: Option<&'a [ResidentKnowledgeEntry]>,
|
||||
/// Resident workflow descriptions from `<workspace>/.insomnia/workflow/*`
|
||||
/// whose frontmatter has `model_invokation: true`. `None` disables the
|
||||
/// section; Phase 2 workers opt out together with resident Knowledge.
|
||||
/// section; consolidation workers opt out together with resident Knowledge.
|
||||
pub resident_workflows: Option<&'a [ResidentWorkflowEntry]>,
|
||||
/// Catalog used to render the fixed trailing section headers.
|
||||
/// Passed by reference so callers do not give up ownership across
|
||||
|
||||
@@ -333,7 +333,7 @@ async fn mid_turn_compact_success_broadcasts_start_and_done() {
|
||||
}
|
||||
|
||||
/// Regression: `Pod::compact()` must reset the in-memory
|
||||
/// `extract_pointer` so Phase 1 keeps firing on the new compacted
|
||||
/// `extract_pointer` so extract keeps firing on the new compacted
|
||||
/// session.
|
||||
///
|
||||
/// Without the reset, the pointer's `processed_through_history_len`
|
||||
@@ -341,7 +341,7 @@ async fn mid_turn_compact_success_broadcasts_start_and_done() {
|
||||
/// session starts with a much shorter history (`[summary, ...]`).
|
||||
/// `cumulative_input_tokens_since` would then filter every new
|
||||
/// usage record out (their `history_len` is below the stale pointer)
|
||||
/// and Phase 1 would never re-fire for the rest of the process.
|
||||
/// and extract would never re-fire for the rest of the process.
|
||||
const EXTRACT_PLUS_COMPACT_MANIFEST: &str = r#"
|
||||
[pod]
|
||||
name = "test-pod"
|
||||
@@ -385,7 +385,7 @@ fn write_extracted_tool_use_events(call_id: &str) -> Vec<LlmEvent> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compact_resets_extract_pointer_so_phase1_can_fire_again() {
|
||||
async fn compact_resets_extract_pointer_so_extract_can_fire_again() {
|
||||
// Mock LLM responses, in call order:
|
||||
// [0] first run with usage(1000) so extract threshold (=1) fires.
|
||||
// [1] extract worker invokes write_extracted with empty payload.
|
||||
@@ -403,7 +403,7 @@ async fn compact_resets_extract_pointer_so_phase1_can_fire_again() {
|
||||
|
||||
pod.run_text("first").await.unwrap();
|
||||
|
||||
// Phase 1 fires; pointer becomes Some.
|
||||
// extract fires; pointer becomes Some.
|
||||
pod.try_post_run_extract().await.unwrap();
|
||||
assert!(
|
||||
pod.extract_pointer().is_some(),
|
||||
@@ -420,8 +420,8 @@ async fn compact_resets_extract_pointer_so_phase1_can_fire_again() {
|
||||
}
|
||||
|
||||
/// `extract_threshold = 0` is treated as "disabled" — without this, a
|
||||
/// raw `>=` comparison against `tokens_since` would fire Phase 1 on
|
||||
/// every post-run regardless of activity. Mirrors the Phase 2
|
||||
/// raw `>=` comparison against `tokens_since` would fire extract on
|
||||
/// every post-run regardless of activity. Mirrors the consolidation
|
||||
/// zero-threshold convention so users have a single way to opt out
|
||||
/// without removing the `[memory]` section.
|
||||
const EXTRACT_THRESHOLD_ZERO_MANIFEST: &str = r#"
|
||||
@@ -446,7 +446,7 @@ permission = "write"
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_threshold_zero_is_disabled() {
|
||||
// Mock provides exactly one response — the first run. If Phase 1
|
||||
// Mock provides exactly one response — the first run. If extract
|
||||
// were treated as "fire on any change" because of `tokens_since >= 0`,
|
||||
// it would call into the extract worker and exhaust the mock.
|
||||
let client = MockClient::new(vec![text_events_with_usage("hi", 1000)]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Phase 2 (memory.consolidation) post-run trigger.
|
||||
//! consolidation (memory.consolidation) post-run trigger.
|
||||
//!
|
||||
//! Covers the gating, lock and cleanup behaviour without exercising the
|
||||
//! full sub-worker tool loop:
|
||||
@@ -203,7 +203,7 @@ async fn no_thresholds_is_a_noop() {
|
||||
let mut pod = make_pod_with(MEMORY_NO_THRESHOLDS_TOML, pwd.path().to_path_buf(), client).await;
|
||||
pod.try_post_run_consolidate()
|
||||
.await
|
||||
.expect("phase 2 disabled when both thresholds are None");
|
||||
.expect("consolidation disabled when both thresholds are None");
|
||||
|
||||
// No staging entries removed.
|
||||
assert_eq!(memory::consolidate::list_staging_entries(&layout).len(), 5);
|
||||
@@ -212,7 +212,7 @@ async fn no_thresholds_is_a_noop() {
|
||||
#[tokio::test]
|
||||
async fn zero_thresholds_treated_as_disabled() {
|
||||
// Without the `Some(0) → None` collapse, `total_files >= 0` and
|
||||
// `total_bytes >= 0` would always evaluate true and Phase 2 would
|
||||
// `total_bytes >= 0` would always evaluate true and consolidation would
|
||||
// fire on every post-run with any staging activity.
|
||||
let pwd = tempfile::tempdir().unwrap();
|
||||
let layout = WorkspaceLayout::new(pwd.path().to_path_buf());
|
||||
@@ -265,7 +265,7 @@ async fn fires_on_threshold_and_cleans_up_consumed_entries() {
|
||||
let layout = WorkspaceLayout::new(pwd.path().to_path_buf());
|
||||
write_n_staging(&layout, 2); // threshold is 2 — fires.
|
||||
|
||||
// Sub-worker is given a single text-only response. The Phase 2 prompt
|
||||
// Sub-worker is given a single text-only response. The consolidation prompt
|
||||
// tells it to call memory tools; the mock skips those, but `Worker::run`
|
||||
// returns Ok regardless once the LLM closes with a final text.
|
||||
let client = MockClient::new(vec![done("ok")]);
|
||||
@@ -343,7 +343,7 @@ async fn coalesce_loop_terminates_with_one_iteration_when_snapshot_drains_stagin
|
||||
|
||||
// Coalesce semantics from `docs/plan/memory.md` §並走防止: a single
|
||||
// run consumes the snapshot taken at acquire time; the loop
|
||||
// re-evaluates against any post-snapshot Phase 1 additions. With no
|
||||
// re-evaluates against any post-snapshot extract additions. With no
|
||||
// concurrent additions, the second iteration sees an empty staging
|
||||
// and bails out — exercised here by counting LLM calls.
|
||||
let pwd = tempfile::tempdir().unwrap();
|
||||
@@ -380,7 +380,7 @@ async fn live_lock_held_by_other_pod_skips() {
|
||||
write_n_staging(&layout, 3);
|
||||
|
||||
// Pre-acquire lock with this test's PID — definitely alive — and
|
||||
// *don't* release it. The Phase 2 path must skip without error.
|
||||
// *don't* release it. The consolidation path must skip without error.
|
||||
let _live_lock = memory::consolidate::StagingLock::acquire(
|
||||
&layout,
|
||||
std::process::id(),
|
||||
|
||||
@@ -178,7 +178,7 @@ pub enum LogEntry {
|
||||
/// `RestoredState.extensions` に `(domain, payload)` を順に積むだけ。
|
||||
/// 各ドメイン側が自前で fold して最新値を取り出す前提。
|
||||
///
|
||||
/// 想定用途: memory subsystem の Phase 1 処理境界 pointer 等、
|
||||
/// 想定用途: memory subsystem の extract 処理境界 pointer 等、
|
||||
/// 「session 寿命に縛りたいが session-store の型を汚したくない」
|
||||
/// メタデータ。
|
||||
Extension {
|
||||
|
||||
Reference in New Issue
Block a user