Compare commits
9 Commits
d818b37f3d
...
aae36f2b56
| Author | SHA1 | Date | |
|---|---|---|---|
| aae36f2b56 | |||
| e418f3996f | |||
| 240b36d738 | |||
| 0356e29707 | |||
| eec33aba98 | |||
| 248e3d7aa2 | |||
| 3beaff7679 | |||
| ef0cdf75e2 | |||
| 5976aac78d |
3
TODO.md
3
TODO.md
|
|
@ -24,8 +24,11 @@
|
|||
- セッションコンテキスト長 / ウィンドウ占有率の常時表示 → [tickets/tui-context-usage-indicator.md](tickets/tui-context-usage-indicator.md)
|
||||
- Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md)
|
||||
- Prune: 保護境界を turn 数から末尾 token budget に置き換え → [tickets/prune-token-budget.md](tickets/prune-token-budget.md)
|
||||
- Compact worker サーキットブレーカーを占有量ベースに統一 → [tickets/compact-worker-occupancy-cap.md](tickets/compact-worker-occupancy-cap.md)
|
||||
- メモリ機構
|
||||
- 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md)
|
||||
- Phase 1/2 呼称を extract/consolidation に統一 → [tickets/memory-phase-naming.md](tickets/memory-phase-naming.md)
|
||||
- extract / consolidation 監査ログ → [tickets/memory-audit-log.md](tickets/memory-audit-log.md)
|
||||
- セッション内 Task ツールの注意機構(無アクティビティで `<system-reminder>` ナッジ) → [tickets/session-todo-reminder.md](tickets/session-todo-reminder.md)
|
||||
- ワークスペースのメモリーをLintするヘッドレスCLI
|
||||
- system-reminder 注入機構の汎用化(2件目の利用者が出た時に検討。タグ形式 `<system-reminder>...</system-reminder>` の規約は session-todo-reminder で先行確立。注入された Item は worker.history に append する方針)
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ pub struct CompactionConfigPartial {
|
|||
#[serde(default)]
|
||||
pub compact_worker_max_input_tokens: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub compact_worker_max_turns: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub model: Option<ModelManifest>,
|
||||
}
|
||||
|
||||
|
|
@ -250,6 +252,9 @@ impl MemoryConfig {
|
|||
extract_worker_max_input_tokens: upper
|
||||
.extract_worker_max_input_tokens
|
||||
.or(self.extract_worker_max_input_tokens),
|
||||
extract_worker_max_turns: upper
|
||||
.extract_worker_max_turns
|
||||
.or(self.extract_worker_max_turns),
|
||||
consolidation_model: upper.consolidation_model.or(self.consolidation_model),
|
||||
consolidation_threshold_files: upper
|
||||
.consolidation_threshold_files
|
||||
|
|
@ -325,6 +330,9 @@ impl CompactionConfigPartial {
|
|||
compact_worker_max_input_tokens: upper
|
||||
.compact_worker_max_input_tokens
|
||||
.or(self.compact_worker_max_input_tokens),
|
||||
compact_worker_max_turns: upper
|
||||
.compact_worker_max_turns
|
||||
.or(self.compact_worker_max_turns),
|
||||
model: merge_option(self.model, upper.model, ModelManifest::merge),
|
||||
}
|
||||
}
|
||||
|
|
@ -461,6 +469,9 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
|||
compact_worker_max_input_tokens: c
|
||||
.compact_worker_max_input_tokens
|
||||
.unwrap_or(defaults::COMPACT_WORKER_MAX_INPUT_TOKENS),
|
||||
compact_worker_max_turns: c
|
||||
.compact_worker_max_turns
|
||||
.or(defaults::COMPACT_WORKER_MAX_TURNS),
|
||||
model: c.model,
|
||||
})
|
||||
})
|
||||
|
|
@ -949,6 +960,32 @@ stop_sequences = ["\n\n", "</stop>"]
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_toml_accepts_compact_worker_max_turns() {
|
||||
let cfg = PodManifestConfig::from_toml(
|
||||
r#"
|
||||
[compaction]
|
||||
compact_worker_max_turns = 7
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(cfg.compaction.unwrap().compact_worker_max_turns, Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_from_compaction_defaults_compact_worker_max_turns() {
|
||||
let mut cfg = minimal_valid();
|
||||
cfg.compaction = Some(CompactionConfigPartial::default());
|
||||
|
||||
let manifest = PodManifest::try_from(cfg).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
manifest.compaction.unwrap().compact_worker_max_turns,
|
||||
defaults::COMPACT_WORKER_MAX_TURNS
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_toml_partial_layer_succeeds() {
|
||||
// A project-layer manifest with only scope set must parse fine.
|
||||
|
|
@ -1042,7 +1079,10 @@ name = "dbg"
|
|||
fn skills_directories_resolved_against_base() {
|
||||
let mut cfg = minimal_valid();
|
||||
cfg.skills = Some(SkillsConfig {
|
||||
directories: vec![PathBuf::from(".claude/skills"), PathBuf::from("/abs/elsewhere")],
|
||||
directories: vec![
|
||||
PathBuf::from(".claude/skills"),
|
||||
PathBuf::from("/abs/elsewhere"),
|
||||
],
|
||||
});
|
||||
let resolved = cfg.resolve_paths(Path::new("/workspace/proj"));
|
||||
let dirs = resolved.skills.as_ref().unwrap().directories.clone();
|
||||
|
|
|
|||
|
|
@ -36,17 +36,25 @@ pub const DEFAULT_INSTRUCTION: &str = "$insomnia/default";
|
|||
/// [`crate::CompactionConfig::compact_auto_read_budget`].
|
||||
pub const COMPACT_AUTO_READ_BUDGET: u64 = 8000;
|
||||
|
||||
/// Cumulative input-token cap for the compact worker's own LLM
|
||||
/// Current prompt-occupancy cap for the compact worker's own LLM
|
||||
/// calls. Exceeding this aborts the compact run (circuit-breaker
|
||||
/// path). See
|
||||
/// [`crate::CompactionConfig::compact_worker_max_input_tokens`].
|
||||
pub const COMPACT_WORKER_MAX_INPUT_TOKENS: u64 = 50_000;
|
||||
|
||||
/// Optional maximum compact-worker tool-loop depth. `None` means unlimited.
|
||||
/// See [`crate::CompactionConfig::compact_worker_max_turns`].
|
||||
pub const COMPACT_WORKER_MAX_TURNS: Option<u32> = Some(20);
|
||||
|
||||
/// Number of recently-touched files fed to the compact worker as
|
||||
/// default references.
|
||||
pub const COMPACT_DEFAULT_REFERENCE_COUNT: usize = 5;
|
||||
|
||||
/// Cumulative input-token cap for the memory Phase 1 (extract) worker's
|
||||
/// own LLM calls. Exceeding this aborts the extract run.
|
||||
/// See [`crate::MemoryConfig::extract_worker_max_input_tokens`].
|
||||
/// Current prompt-occupancy cap for the memory extract worker's own
|
||||
/// LLM requests. Exceeding this aborts the extract run (circuit-breaker
|
||||
/// path). See [`crate::MemoryConfig::extract_worker_max_input_tokens`].
|
||||
pub const MEMORY_EXTRACT_WORKER_MAX_INPUT_TOKENS: u64 = 30_000;
|
||||
|
||||
/// Optional maximum extract-worker tool-loop depth. `None` means unlimited.
|
||||
/// See [`crate::MemoryConfig::extract_worker_max_turns`].
|
||||
pub const MEMORY_EXTRACT_WORKER_MAX_TURNS: Option<u32> = Some(8);
|
||||
|
|
|
|||
|
|
@ -96,37 +96,44 @@ 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)]
|
||||
pub extract_threshold: Option<u64>,
|
||||
/// Cumulative input-token cap for the extract worker's own LLM
|
||||
/// calls. Exceeding this aborts the extract run. `None` ⇒
|
||||
/// Current prompt-occupancy cap for the extract worker's own LLM
|
||||
/// requests. Exceeding this aborts the extract run. `None` ⇒
|
||||
/// [`defaults::MEMORY_EXTRACT_WORKER_MAX_INPUT_TOKENS`].
|
||||
#[serde(default)]
|
||||
pub extract_worker_max_input_tokens: Option<u64>,
|
||||
/// Optional model for the Phase 2 (consolidation) worker. When
|
||||
/// Optional maximum extract-worker tool-loop depth. `None` leaves
|
||||
/// the worker unlimited; the default bounds runaway short-context
|
||||
/// loops. Falls through to
|
||||
/// [`defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS`] when unset.
|
||||
#[serde(default)]
|
||||
pub extract_worker_max_turns: Option<u32>,
|
||||
/// 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>,
|
||||
}
|
||||
|
|
@ -321,11 +328,16 @@ pub struct CompactionConfig {
|
|||
#[serde(default = "default_compact_auto_read_budget")]
|
||||
pub compact_auto_read_budget: u64,
|
||||
|
||||
/// Cumulative input-token cap for the compact worker's own LLM
|
||||
/// calls. Exceeding this aborts the compact run.
|
||||
/// Current prompt-occupancy cap for the compact worker's own LLM
|
||||
/// requests. Exceeding this aborts the compact run.
|
||||
#[serde(default = "default_compact_worker_max_input_tokens")]
|
||||
pub compact_worker_max_input_tokens: u64,
|
||||
|
||||
/// Optional maximum compact-worker tool-loop depth. `None` leaves the
|
||||
/// worker unlimited; the default bounds runaway short-context loops.
|
||||
#[serde(default = "default_compact_worker_max_turns")]
|
||||
pub compact_worker_max_turns: Option<u32>,
|
||||
|
||||
/// Optional model for the compactor (summary) LLM.
|
||||
/// If omitted, the main model is cloned via `clone_boxed()`.
|
||||
#[serde(default)]
|
||||
|
|
@ -347,6 +359,9 @@ fn default_compact_auto_read_budget() -> u64 {
|
|||
fn default_compact_worker_max_input_tokens() -> u64 {
|
||||
defaults::COMPACT_WORKER_MAX_INPUT_TOKENS
|
||||
}
|
||||
fn default_compact_worker_max_turns() -> Option<u32> {
|
||||
defaults::COMPACT_WORKER_MAX_TURNS
|
||||
}
|
||||
|
||||
impl Default for CompactionConfig {
|
||||
fn default() -> Self {
|
||||
|
|
@ -358,6 +373,7 @@ impl Default for CompactionConfig {
|
|||
compact_retained_tokens: default_compact_retained_tokens(),
|
||||
compact_auto_read_budget: default_compact_auto_read_budget(),
|
||||
compact_worker_max_input_tokens: default_compact_worker_max_input_tokens(),
|
||||
compact_worker_max_turns: default_compact_worker_max_turns(),
|
||||
model: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -521,6 +537,19 @@ model_id = "claude-sonnet-4-20250514"
|
|||
assert_eq!(c.compact_threshold, Some(80000));
|
||||
assert_eq!(c.compact_request_threshold, None);
|
||||
assert_eq!(c.compact_retained_tokens, 8000);
|
||||
assert_eq!(c.compact_worker_max_turns, Some(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_compaction_worker_max_turns() {
|
||||
let toml = format!(
|
||||
"{MINIMAL_REQUIRED}\n\
|
||||
[compaction]\n\
|
||||
compact_worker_max_turns = 7\n"
|
||||
);
|
||||
let manifest = PodManifest::from_toml(&toml).unwrap();
|
||||
let c = manifest.compaction.unwrap();
|
||||
assert_eq!(c.compact_worker_max_turns, Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -99,6 +99,18 @@ impl UsageTracker {
|
|||
});
|
||||
}
|
||||
|
||||
/// Return a clone of the accumulated `UsageRecord`s without clearing them.
|
||||
/// Used by request-time circuit breakers that need the same occupancy
|
||||
/// projection as Pod persistence while the run is still active.
|
||||
pub(crate) fn records(&self) -> Vec<UsageRecord> {
|
||||
self.pending_records
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| r.record.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Drain accumulated records. Called by Pod after a run completes,
|
||||
/// before persisting the turn.
|
||||
pub(crate) fn drain(&self) -> Vec<RecordedUsage> {
|
||||
|
|
@ -136,6 +148,19 @@ mod tests {
|
|||
assert!(records[0].correlation_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_clones_without_clearing() {
|
||||
let tracker = UsageTracker::new();
|
||||
tracker.note_request(1);
|
||||
tracker.record_usage(&make_event(10, 0, 0, 5));
|
||||
|
||||
let records = tracker.records();
|
||||
assert_eq!(records.len(), 1);
|
||||
assert_eq!(records[0].history_len, 1);
|
||||
assert_eq!(records[0].input_total_tokens, 10);
|
||||
assert_eq!(tracker.records().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_clears_buffer() {
|
||||
let tracker = UsageTracker::new();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
//! compacted session's opening system messages.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
|
@ -28,6 +27,7 @@ use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
|||
use serde::Deserialize;
|
||||
use tools::ScopedFs;
|
||||
|
||||
use crate::compact::usage_tracker::UsageTracker;
|
||||
use crate::fs_view::{ReadRequirement, slice_lines};
|
||||
|
||||
/// Aggregated output of a compact worker run.
|
||||
|
|
@ -246,24 +246,29 @@ pub(crate) fn write_summary_tool(ctx: Arc<Mutex<CompactWorkerContext>>) -> ToolD
|
|||
})
|
||||
}
|
||||
|
||||
/// Interceptor that aborts the compact worker as soon as its cumulative
|
||||
/// input-token count crosses `max_input_tokens`. Pairs with the
|
||||
/// `on_usage` callback registered by `Pod::compact`, which is what
|
||||
/// actually accumulates `input_so_far`.
|
||||
/// Interceptor that aborts the compact worker when its current prompt
|
||||
/// occupancy estimate crosses `max_input_tokens`. The estimate uses the same
|
||||
/// `UsageRecord` + `llm_worker::token_counter::total_tokens` path as the main
|
||||
/// Pod compaction thresholds, so prompt-cache hits are not counted cumulatively
|
||||
/// across turns.
|
||||
pub(crate) struct CompactWorkerInterceptor {
|
||||
pub input_so_far: Arc<AtomicU64>,
|
||||
pub usage_tracker: Arc<UsageTracker>,
|
||||
pub max_input_tokens: u64,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Interceptor for CompactWorkerInterceptor {
|
||||
async fn pre_llm_request(&self, _context: &mut Vec<Item>) -> PreRequestAction {
|
||||
if self.input_so_far.load(Ordering::Relaxed) > self.max_input_tokens {
|
||||
async fn pre_llm_request(&self, context: &mut Vec<Item>) -> PreRequestAction {
|
||||
let records = self.usage_tracker.records();
|
||||
let estimate = llm_worker::token_counter::total_tokens(context, &records);
|
||||
if estimate.tokens > self.max_input_tokens {
|
||||
return PreRequestAction::Cancel(format!(
|
||||
"compact worker input exceeded {} tokens",
|
||||
"compact worker input occupancy exceeded {} tokens",
|
||||
self.max_input_tokens
|
||||
));
|
||||
}
|
||||
|
||||
self.usage_tracker.note_request(context.len());
|
||||
PreRequestAction::Continue
|
||||
}
|
||||
}
|
||||
|
|
@ -283,6 +288,66 @@ mod tests {
|
|||
ScopedFs::new(scope, tmp.to_path_buf())
|
||||
}
|
||||
|
||||
fn make_usage(input: u64) -> llm_worker::timeline::event::UsageEvent {
|
||||
llm_worker::timeline::event::UsageEvent {
|
||||
input_tokens: Some(input),
|
||||
output_tokens: Some(0),
|
||||
total_tokens: Some(input),
|
||||
cache_read_input_tokens: None,
|
||||
cache_creation_input_tokens: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compact_worker_interceptor_uses_occupancy_not_cumulative_usage() {
|
||||
let tracker = Arc::new(UsageTracker::new());
|
||||
let interceptor = CompactWorkerInterceptor {
|
||||
usage_tracker: tracker.clone(),
|
||||
max_input_tokens: 150,
|
||||
};
|
||||
let mut context = vec![Item::user_message("hello")];
|
||||
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Continue
|
||||
));
|
||||
tracker.record_usage(&make_usage(100));
|
||||
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Continue
|
||||
));
|
||||
tracker.record_usage(&make_usage(100));
|
||||
|
||||
// Two 100-token requests would exceed a cumulative 150-token cap, but
|
||||
// current occupancy is still the latest 100-token measurement.
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Continue
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compact_worker_interceptor_cancels_when_occupancy_exceeds_cap() {
|
||||
let tracker = Arc::new(UsageTracker::new());
|
||||
let interceptor = CompactWorkerInterceptor {
|
||||
usage_tracker: tracker.clone(),
|
||||
max_input_tokens: 99,
|
||||
};
|
||||
let mut context = vec![Item::user_message("hello")];
|
||||
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Continue
|
||||
));
|
||||
tracker.record_usage(&make_usage(100));
|
||||
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Cancel(message) if message.contains("occupancy")
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mark_read_required_records_and_deducts_budget() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ use session_store::{EntryHash, PodScopeSnapshot, SessionId, SessionStartState, S
|
|||
use tracing::{info, warn};
|
||||
|
||||
use manifest::{
|
||||
Permission, PodManifest, PodManifestConfig, ResolveError, Scope, ScopeConfig, ScopeError, ScopeRule,
|
||||
SharedScope, WorkerManifest,
|
||||
Permission, PodManifest, PodManifestConfig, ResolveError, Scope, ScopeConfig, ScopeError,
|
||||
ScopeRule, SharedScope, WorkerManifest,
|
||||
};
|
||||
|
||||
use crate::compact::state::CompactState;
|
||||
|
|
@ -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 {
|
||||
|
|
@ -1456,8 +1456,6 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
///
|
||||
/// Returns the new session ID.
|
||||
pub async fn compact(&mut self, retained_tokens: u64) -> Result<SessionId, PodError> {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use crate::compact::worker::{
|
||||
CompactWorkerContext, CompactWorkerInterceptor, add_reference_tool,
|
||||
mark_read_required_tool, write_summary_tool,
|
||||
|
|
@ -1477,7 +1475,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
|
||||
// Compaction-related knobs. Fall through to manifest defaults when
|
||||
// `[compaction]` is omitted entirely.
|
||||
let (auto_read_budget, compact_worker_max_input_tokens) = self
|
||||
let (auto_read_budget, compact_worker_max_input_tokens, compact_worker_max_turns) = self
|
||||
.manifest
|
||||
.compaction
|
||||
.as_ref()
|
||||
|
|
@ -1485,11 +1483,13 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
(
|
||||
c.compact_auto_read_budget,
|
||||
c.compact_worker_max_input_tokens,
|
||||
c.compact_worker_max_turns,
|
||||
)
|
||||
})
|
||||
.unwrap_or((
|
||||
manifest::defaults::COMPACT_AUTO_READ_BUDGET,
|
||||
manifest::defaults::COMPACT_WORKER_MAX_INPUT_TOKENS,
|
||||
manifest::defaults::COMPACT_WORKER_MAX_TURNS,
|
||||
));
|
||||
|
||||
// Default references: the N most-recently-touched files in the
|
||||
|
|
@ -1530,21 +1530,22 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
let mut summary_worker = Worker::new(summary_client).system_prompt(summary_system_prompt);
|
||||
summary_worker.set_cache_key(Some(self.session_id.to_string()));
|
||||
|
||||
// Cumulative input-token meter + interceptor. The meter is bumped
|
||||
// from the on_usage callback and read on every pre_llm_request.
|
||||
let input_so_far = Arc::new(AtomicU64::new(0));
|
||||
// Occupancy-based input-token meter + interceptor. The tracker pairs
|
||||
// each pre-request history length with the following UsageEvent, then
|
||||
// the interceptor projects current prompt occupancy with the same
|
||||
// UsageRecord counter used by the main Pod thresholds.
|
||||
let summary_usage_tracker = Arc::new(UsageTracker::new());
|
||||
{
|
||||
let acc = input_so_far.clone();
|
||||
let tracker = summary_usage_tracker.clone();
|
||||
summary_worker.on_usage(move |event| {
|
||||
if let Some(tokens) = event.input_tokens {
|
||||
acc.fetch_add(tokens, Ordering::Relaxed);
|
||||
}
|
||||
tracker.record_usage(event);
|
||||
});
|
||||
}
|
||||
summary_worker.set_interceptor(CompactWorkerInterceptor {
|
||||
input_so_far: input_so_far.clone(),
|
||||
usage_tracker: summary_usage_tracker,
|
||||
max_input_tokens: compact_worker_max_input_tokens,
|
||||
});
|
||||
summary_worker.set_max_turns(compact_worker_max_turns);
|
||||
|
||||
// Tools: read_file (shared scope, fresh tracker) + the three
|
||||
// compact-specific tools that populate `ctx`.
|
||||
|
|
@ -1732,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
|
||||
|
|
@ -1763,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.
|
||||
|
|
@ -1779,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)` の差分で、
|
||||
|
|
@ -1798,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
|
||||
|
|
@ -1844,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(());
|
||||
}
|
||||
|
|
@ -1914,6 +1915,9 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
let cap = memory_cfg
|
||||
.extract_worker_max_input_tokens
|
||||
.unwrap_or(manifest::defaults::MEMORY_EXTRACT_WORKER_MAX_INPUT_TOKENS);
|
||||
let extract_worker_max_turns = memory_cfg
|
||||
.extract_worker_max_turns
|
||||
.or(manifest::defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS);
|
||||
|
||||
let client = self.build_extractor_client(memory_cfg)?;
|
||||
let extract_system_prompt = self
|
||||
|
|
@ -1923,22 +1927,22 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
let mut extract_worker = Worker::new(client).system_prompt(extract_system_prompt);
|
||||
extract_worker.set_cache_key(Some(self.session_id.to_string()));
|
||||
|
||||
// Cumulative input-token meter + interceptor (mirror of
|
||||
// CompactWorkerInterceptor). Aborts the extract worker if its
|
||||
// own input usage crosses the cap.
|
||||
let input_so_far = Arc::new(std::sync::atomic::AtomicU64::new(0));
|
||||
// Occupancy-based input-token meter + interceptor. The tracker pairs
|
||||
// each pre-request history length with the following UsageEvent, then
|
||||
// the interceptor projects current prompt occupancy with the same
|
||||
// UsageRecord counter used by the main Pod thresholds.
|
||||
let extract_usage_tracker = Arc::new(UsageTracker::new());
|
||||
{
|
||||
let acc = input_so_far.clone();
|
||||
let tracker = extract_usage_tracker.clone();
|
||||
extract_worker.on_usage(move |event| {
|
||||
if let Some(tokens) = event.input_tokens {
|
||||
acc.fetch_add(tokens, Ordering::Relaxed);
|
||||
}
|
||||
tracker.record_usage(event);
|
||||
});
|
||||
}
|
||||
extract_worker.set_interceptor(MemoryExtractWorkerInterceptor {
|
||||
input_so_far: input_so_far.clone(),
|
||||
usage_tracker: extract_usage_tracker,
|
||||
max_input_tokens: cap,
|
||||
});
|
||||
extract_worker.set_max_turns(extract_worker_max_turns);
|
||||
|
||||
let ctx = Arc::new(extract::ExtractWorkerContext::new());
|
||||
extract_worker.register_tool(extract::write_extracted_tool(ctx.clone()));
|
||||
|
|
@ -1951,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()
|
||||
|
|
@ -1998,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`].
|
||||
|
|
@ -2014,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
|
||||
|
|
@ -2031,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);
|
||||
|
|
@ -2058,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(());
|
||||
}
|
||||
|
|
@ -2131,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);
|
||||
|
|
@ -2163,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.
|
||||
|
|
@ -2172,12 +2176,15 @@ enum ExtractDecision {
|
|||
Completed,
|
||||
}
|
||||
|
||||
/// Pre-request interceptor for the Phase 1 extract worker. Aborts when
|
||||
/// cumulative input tokens cross `max_input_tokens`. Mirror of
|
||||
/// `compact::worker::CompactWorkerInterceptor`; kept separate so each
|
||||
/// subsystem can tune its own message and budget.
|
||||
/// Pre-request interceptor for the extract worker. Aborts when current
|
||||
/// prompt occupancy crosses `max_input_tokens`. Uses the same
|
||||
/// `UsageRecord` + `llm_worker::token_counter::total_tokens` projection
|
||||
/// as the main Pod compaction thresholds, so prompt-cache hits are not
|
||||
/// counted cumulatively across turns. Kept separate from
|
||||
/// `compact::worker::CompactWorkerInterceptor` so each subsystem can
|
||||
/// tune its own cancel message and budget.
|
||||
struct MemoryExtractWorkerInterceptor {
|
||||
input_so_far: Arc<std::sync::atomic::AtomicU64>,
|
||||
usage_tracker: Arc<UsageTracker>,
|
||||
max_input_tokens: u64,
|
||||
}
|
||||
|
||||
|
|
@ -2185,19 +2192,23 @@ struct MemoryExtractWorkerInterceptor {
|
|||
impl llm_worker::interceptor::Interceptor for MemoryExtractWorkerInterceptor {
|
||||
async fn pre_llm_request(
|
||||
&self,
|
||||
_context: &mut Vec<Item>,
|
||||
context: &mut Vec<Item>,
|
||||
) -> llm_worker::interceptor::PreRequestAction {
|
||||
if self.input_so_far.load(Ordering::Relaxed) > self.max_input_tokens {
|
||||
let records = self.usage_tracker.records();
|
||||
let estimate = llm_worker::token_counter::total_tokens(context, &records);
|
||||
if estimate.tokens > self.max_input_tokens {
|
||||
return llm_worker::interceptor::PreRequestAction::Cancel(format!(
|
||||
"Phase 1 extract worker input exceeded {} tokens",
|
||||
"extract worker input occupancy exceeded {} tokens",
|
||||
self.max_input_tokens
|
||||
));
|
||||
}
|
||||
|
||||
self.usage_tracker.note_request(context.len());
|
||||
llm_worker::interceptor::PreRequestAction::Continue
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
|
@ -2711,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}")]
|
||||
|
|
@ -3069,8 +3080,79 @@ permission = "write"
|
|||
let shadows = ingest_skills(&mut registry, &manifest);
|
||||
|
||||
// workspace skill `alpha` should be registered (no collision).
|
||||
assert!(registry.get(&memory::Slug::parse("alpha").unwrap()).is_some());
|
||||
assert!(
|
||||
registry
|
||||
.get(&memory::Slug::parse("alpha").unwrap())
|
||||
.is_some()
|
||||
);
|
||||
// No workflow exists to shadow `alpha`, so no shadow event for it.
|
||||
assert!(shadows.iter().all(|s| s.slug.as_str() != "alpha"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod memory_extract_interceptor_tests {
|
||||
use super::*;
|
||||
use llm_worker::interceptor::{Interceptor, PreRequestAction};
|
||||
use llm_worker::timeline::event::UsageEvent;
|
||||
|
||||
fn make_usage(input: u64) -> UsageEvent {
|
||||
UsageEvent {
|
||||
input_tokens: Some(input),
|
||||
output_tokens: Some(0),
|
||||
total_tokens: Some(input),
|
||||
cache_read_input_tokens: None,
|
||||
cache_creation_input_tokens: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_interceptor_uses_occupancy_not_cumulative_usage() {
|
||||
let tracker = Arc::new(UsageTracker::new());
|
||||
let interceptor = MemoryExtractWorkerInterceptor {
|
||||
usage_tracker: tracker.clone(),
|
||||
max_input_tokens: 150,
|
||||
};
|
||||
let mut context = vec![Item::user_message("hello")];
|
||||
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Continue
|
||||
));
|
||||
tracker.record_usage(&make_usage(100));
|
||||
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Continue
|
||||
));
|
||||
tracker.record_usage(&make_usage(100));
|
||||
|
||||
// Two 100-token requests would exceed a cumulative 150-token cap, but
|
||||
// current occupancy is still the latest 100-token measurement.
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Continue
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extract_interceptor_cancels_when_occupancy_exceeds_cap() {
|
||||
let tracker = Arc::new(UsageTracker::new());
|
||||
let interceptor = MemoryExtractWorkerInterceptor {
|
||||
usage_tracker: tracker.clone(),
|
||||
max_input_tokens: 99,
|
||||
};
|
||||
let mut context = vec![Item::user_message("hello")];
|
||||
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Continue
|
||||
));
|
||||
tracker.record_usage(&make_usage(100));
|
||||
|
||||
assert!(matches!(
|
||||
interceptor.pre_llm_request(&mut context).await,
|
||||
PreRequestAction::Cancel(message) if message.contains("occupancy")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -140,7 +140,8 @@ compact_threshold = 80000 # ターンの合間 (proactive)
|
|||
compact_request_threshold = 90000 # リクエストの合間 (safety net)
|
||||
retained_tokens = 8000 # 直近保護トークン数 (Prune 済みで計測)
|
||||
auto_read_budget = 8000 # compact worker の mark_read_required 合計上限
|
||||
compact_worker_max_input_tokens = 50000 # compact worker 自身の累計入力トークン上限
|
||||
compact_worker_max_input_tokens = 50000 # compact worker 自身の現在占有トークン上限
|
||||
compact_worker_max_turns = 20 # compact worker 自身の tool loop 上限
|
||||
```
|
||||
|
||||
### Auto-Read とリファレンス
|
||||
|
|
|
|||
|
|
@ -212,9 +212,13 @@ permission = "write"
|
|||
# compact_auto_read_budget = 8000
|
||||
#
|
||||
# # 任意。デフォルト: 50000 (`defaults::COMPACT_WORKER_MAX_INPUT_TOKENS`)。
|
||||
# # compact worker 自身の累積入力 token cap。超過で abort (circuit breaker)。
|
||||
# # compact worker 自身の現在占有 token cap。超過で abort (circuit breaker)。
|
||||
# compact_worker_max_input_tokens = 50000
|
||||
#
|
||||
# # 任意。デフォルト: 20 (`defaults::COMPACT_WORKER_MAX_TURNS`)。
|
||||
# # compact worker 自身の tool loop 上限。Rust config で None の場合のみ無制限。
|
||||
# compact_worker_max_turns = 20
|
||||
#
|
||||
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
|
||||
# # compact 専用モデルを使う場合のみ書く ([model] と同じ形式)。
|
||||
# # [compaction.model]
|
||||
|
|
@ -243,32 +247,36 @@ permission = "write"
|
|||
# query_excerpt_lines = 3
|
||||
#
|
||||
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
|
||||
# # Phase 1 (extract) ワーカーのモデル ([model] と同じ形式)。
|
||||
# # extract ワーカーのモデル ([model] と同じ形式)。
|
||||
# # Haiku / 4o-mini / Flash クラスの軽量 reasoning モデル推奨。
|
||||
# # [memory.extract_model]
|
||||
# # ref = "anthropic/claude-haiku-4-5"
|
||||
#
|
||||
# # 任意。デフォルト: なし (Phase 1 自動発火を完全停止)。
|
||||
# # 前回 extract pointer 以降の累積入力 token がこの値を超えると Phase 1 起動。
|
||||
# # 任意。デフォルト: なし (extract 自動発火を完全停止)。
|
||||
# # 前回 extract pointer 以降の累積入力 token がこの値を超えると extract 起動。
|
||||
# # ※ memory tools と resident injection は extract_threshold が None でも動く。
|
||||
# extract_threshold = 30000
|
||||
#
|
||||
# # 任意。デフォルト: 30000 (`defaults::MEMORY_EXTRACT_WORKER_MAX_INPUT_TOKENS`)。
|
||||
# # extract worker 自身の累積入力 token cap (超過で abort)。
|
||||
# # extract worker 自身の現在占有 token cap (超過で abort)。
|
||||
# extract_worker_max_input_tokens = 30000
|
||||
#
|
||||
# # 任意。デフォルト: 8 (`defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS`)。
|
||||
# # extract worker 自身の tool loop 上限。Rust config で None の場合のみ無制限。
|
||||
# extract_worker_max_turns = 8
|
||||
#
|
||||
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
|
||||
# # Phase 2 (consolidation) ワーカーのモデル。reasoning クラス推奨。
|
||||
# # consolidation ワーカーのモデル。reasoning クラス推奨。
|
||||
# # [memory.consolidation_model]
|
||||
# # ref = "anthropic/claude-sonnet-4-6"
|
||||
#
|
||||
# # 任意。デフォルト: なし。
|
||||
# # `_staging/` のエントリ数がこの値以上で Phase 2 発火 (files / bytes は OR)。
|
||||
# # `_staging/` のエントリ数がこの値以上で consolidation 発火 (files / bytes は OR)。
|
||||
# consolidation_threshold_files = 50
|
||||
#
|
||||
# # 任意。デフォルト: なし。
|
||||
# # `_staging/` の総バイト数がこの値以上で Phase 2 発火 (files / bytes は OR)。
|
||||
# # files / bytes の両方が None だと Phase 2 完全無効。
|
||||
# # `_staging/` の総バイト数がこの値以上で consolidation 発火 (files / bytes は OR)。
|
||||
# # files / bytes の両方が None だと consolidation 完全無効。
|
||||
# consolidation_threshold_bytes = 1048576
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
## Context
|
||||
|
||||
`docs/plan/memory.md` で定義した永続化・検索・Phase 1 / Phase 2 の基盤は実装済みだが、現状の workspace memory を見る限り、実用上の効果はまだ弱い。
|
||||
`docs/plan/memory.md` で定義した永続化・検索・extract / consolidation の基盤は実装済みだが、現状の workspace memory を見る限り、実用上の効果はまだ弱い。
|
||||
|
||||
現在の主要な症状:
|
||||
|
||||
- `knowledge/*` が空で、通常 Pod が自発的に参照できる discovery 面がない
|
||||
- `memory/summary.md` は「Always-on サマリ」と設計されているが、通常 Pod の system prompt へ常駐注入されていない
|
||||
- Phase 2 は `KnowledgeCandidateReport::empty()` を受け取り、prompt 上も「候補レポートが空なら新規 Knowledge を作るな」としているため、Knowledge の cold-start が起きない
|
||||
- consolidation は `KnowledgeCandidateReport::empty()` を受け取り、prompt 上も「候補レポートが空なら新規 Knowledge を作るな」としているため、Knowledge の cold-start が起きない
|
||||
- `decisions/*` と `requests/*` は記録としては残るが、description / resident injection を持たず、後続 turn で自然に読まれにくい
|
||||
- bundled prompt に INSOMNIA 開発固有の ticket / TODO 運用を前提にした shadow 禁止が入り、一般ユーザー workspace の管理文脈を過剰に落とす可能性がある
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ Usage metrics の前に、memory effectiveness の最小改善として以下を
|
|||
通常 Pod の system prompt trailing section に `summary.md` の本文または圧縮表示を載せる。
|
||||
|
||||
- `[memory]` が有効で、`memory/summary.md` が存在する場合だけ注入する
|
||||
- Phase 2 / internal disposable Worker には注入しない
|
||||
- consolidation / internal disposable Worker には注入しない
|
||||
- 既存の `Resident knowledge` とは別 section にする
|
||||
- summary の frontmatter は除外し、本文のみを載せる
|
||||
- summary 本文の linter 上限は 20,000 chars だが、resident 注入では別途 soft cap を持つか、当初は summary 自体を 1-5k tokens に保つ prompt 方針に依存する
|
||||
|
|
@ -52,14 +52,14 @@ Usage metrics の前に、memory effectiveness の最小改善として以下を
|
|||
|
||||
### Problem
|
||||
|
||||
現行 Phase 2 は `KnowledgeCandidateReport::empty()` を渡し、prompt も空レポート時の新規 Knowledge 作成を禁止している。これは usage metrics 完成後の gate としては妥当だが、cold-start では永久に Knowledge が生まれない。
|
||||
現行 consolidation は `KnowledgeCandidateReport::empty()` を渡し、prompt も空レポート時の新規 Knowledge 作成を禁止している。これは usage metrics 完成後の gate としては妥当だが、cold-start では永久に Knowledge が生まれない。
|
||||
|
||||
### Direction
|
||||
|
||||
Knowledge 作成 gate と resident injection gate を分離する。
|
||||
|
||||
- 新規 Knowledge 作成:
|
||||
- Phase 2 が high-confidence に再利用価値を判断できる場合は許可する
|
||||
- consolidation が high-confidence に再利用価値を判断できる場合は許可する
|
||||
- default は `model_invokation: false`
|
||||
- `description` は「本文要約」ではなく「何の知識で、いつ読むべきか」を書かせる
|
||||
- `model_invokation: true` への昇格:
|
||||
|
|
@ -124,7 +124,7 @@ Memory extraction / consolidation が、ユーザー workspace の管理文脈
|
|||
|
||||
### Direction
|
||||
|
||||
Phase 2 の tidy / consolidation に、既存 `decisions/*` / `requests/*` を Knowledge へ昇格する経路を追加する。
|
||||
consolidation の tidy / consolidation に、既存 `decisions/*` / `requests/*` を Knowledge へ昇格する経路を追加する。
|
||||
|
||||
- 同一内容を copy するのではなく、Knowledge として再利用可能な抽象に rewrite する
|
||||
- 元 decision は必要なら残す。Knowledge 側には `last_sources` として直近 source を持つ
|
||||
|
|
@ -145,7 +145,7 @@ Phase 2 の tidy / consolidation に、既存 `decisions/*` / `requests/*` を K
|
|||
2. Summary resident injection
|
||||
- 既存 `summary.md` が即座に通常 Pod へ効く
|
||||
3. Knowledge cold-start gate
|
||||
- Phase 2 が Knowledge を生めるようにする
|
||||
- consolidation が Knowledge を生めるようにする
|
||||
4. Existing record promotion
|
||||
- 既存 decisions / requests から Knowledge を立ち上げる
|
||||
5. Explicit slug invocation path
|
||||
|
|
@ -170,14 +170,14 @@ Usage metrics は不要ではない。役割を以下に限定すると妥当性
|
|||
|
||||
`summary.md` を常駐させると system prompt budget を消費する。summary が肥大化した場合に備え、以下のどちらかを選ぶ必要がある。
|
||||
|
||||
- linter / Phase 2 prompt で summary を 1-5k tokens に保つ運用を先行する
|
||||
- linter / consolidation prompt で summary を 1-5k tokens に保つ運用を先行する
|
||||
- resident injection 側に hard truncation / warning を入れる
|
||||
|
||||
初期は前者でよいが、truncation されるなら LLM に明示した方がよい。
|
||||
|
||||
### Knowledge noise
|
||||
|
||||
cold-start gate を緩めると Knowledge が増えすぎる可能性がある。対策として、新規作成時の `model_invokation: false` default、update 優先、description 品質指示、tidy phase の noisy 分類を使う。
|
||||
cold-start gate を緩めると Knowledge が増えすぎる可能性がある。対策として、新規作成時の `model_invokation: false` default、update 優先、description 品質指示、tidy step の noisy 分類を使う。
|
||||
|
||||
### Prompt override boundary
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ memory 関連 prompt は種別を問わず、最低限以下を共有する:
|
|||
- **特定の project-management 手法を前提にしない**。既定 prompt は issue tracker / task board / planning doc / changelog / version-control history / generated report などを「正確な内容の authoritative record」として一般化して扱う。memory はそれらの本文・status 変化・短命 identifier をそのまま複製する parallel ledger にはしない。一方で、今後の作業に役立つ持続的な project-management 事実、workflow 制約、優先度判断の根拠、複数 item にまたがる学びは採用してよい
|
||||
- **空出力を許容**する。保存価値が無ければ「何も追加しない」を正当な結果として扱う
|
||||
|
||||
### Phase 1: 活動抽出 prompt
|
||||
### extract: 活動抽出 prompt
|
||||
|
||||
Phase 1 は「派生物を作る」段階ではなく、「起きたことを抽出する」段階として縛る:
|
||||
extract は「派生物を作る」段階ではなく、「起きたことを抽出する」段階として縛る:
|
||||
|
||||
- 対象は `decisions`、`discussions`、`attempts`、`requests` の候補に限る
|
||||
- Knowledge 化、summary rewrite、slug 命名、`model_invokation` 判断は行わない
|
||||
|
|
@ -37,9 +37,9 @@ Phase 1 は「派生物を作る」段階ではなく、「起きたことを抽
|
|||
- `decisions`: rationale が「この session で X をした」になるものは除外。設計 / 方針 / process / 取り組み方の根拠でない記録は decision ではなく作業ログ。ただし、作業管理上の持続的な方針や抽象化は decision として扱える
|
||||
- authoritative project record の title / body / checklist / raw status / 短命 identifier を本文に複製しない。正確な status mirror や identifier と組み合わせないと意味を成さない記録は採用しない
|
||||
|
||||
### Phase 2: 統合 + 整理 prompt
|
||||
### consolidation: 統合 + 整理 prompt
|
||||
|
||||
Phase 2 は既存 `memory/*`、`knowledge/*`、staging を見て、統合 phase と整理 phase を 1 セッション内で続けて回す。両 phase に共通する原則:
|
||||
consolidation は既存 `memory/*`、`knowledge/*`、staging を見て、統合 step と整理 step を 1 セッション内で続けて回す。両 step に共通する原則:
|
||||
|
||||
- 入力には staging の活動ログ、既存 `memory/*`(summary / decisions / requests)の全文、Knowledge 化候補レポート、整理材料(使用頻度メトリクス、Linter Warn、`replaced` chain、sources 過多情報)を含める
|
||||
- 既存 `knowledge/*` は prompt に埋めず、Knowledge 検索ツール経由で agent が必要分を引く。まず候補レポートの source や staging の話題に近い slug を検索し、ヒットした slug / description / kind / `model_invokation` を見て適合先を探す
|
||||
|
|
@ -49,7 +49,7 @@ Phase 2 は既存 `memory/*`、`knowledge/*`、staging を見て、統合 phase
|
|||
- Decision の置き換えは `status: replaced` と `replaced_by` で表現する
|
||||
- 人間編集との不整合が見える rewrite は避け、衝突しそうなら保守的に統合する
|
||||
|
||||
統合 phase の追加指示:
|
||||
統合 step の追加指示:
|
||||
|
||||
- staging の活動ログを decisions / requests / summary / Knowledge update に落とし込む
|
||||
- staging の field ごとに宛先を分ける:
|
||||
|
|
@ -57,9 +57,9 @@ Phase 2 は既存 `memory/*`、`knowledge/*`、staging を見て、統合 phase
|
|||
- `requests` (staging) → `memory/requests/`
|
||||
- `attempts` (staging) → 既定は drop。memory に `attempts/` フォルダは設けない。複数 attempts に通底する持続的な傾向だけ `summary.md` に 1 行で圧縮する例外あり
|
||||
- `discussions` (staging) → 設計 / 方針に決着していれば `decisions/` に統合、未決着でも問い自体が持続的なら `summary.md` に 1 行、それ以外は drop。`decisions/` に「議論した」だけの未決着メモを作らない
|
||||
- Knowledge 新規作成は候補レポート掲載 source 由来に限る(詳細は §Phase 2: Knowledge 書き込み prompt)
|
||||
- Knowledge 新規作成は候補レポート掲載 source 由来に限る(詳細は §Consolidation: Knowledge 書き込み prompt)
|
||||
|
||||
整理 phase の追加指示(統合 phase 完了後、余力で実行):
|
||||
整理 step の追加指示(統合 step 完了後、余力で実行):
|
||||
|
||||
- 既存 record 群を `outdated`、`superseded`、`unused`、`noisy` の観点で評価し、なぜ整理対象なのかを分類する
|
||||
- 明示 invoke の保護閾値超過 record は drop / 大幅圧縮の対象外とする
|
||||
|
|
@ -67,9 +67,9 @@ Phase 2 は既存 `memory/*`、`knowledge/*`、staging を見て、統合 phase
|
|||
- merge / split / trim / drop の理由を git diff から読める形で残す
|
||||
- 直接削除してよいが、git で可逆である前提に甘えすぎず、誤判定しやすいものは merge / trim を優先する
|
||||
|
||||
### Phase 2: Knowledge 書き込み prompt
|
||||
### consolidation: Knowledge 書き込み prompt
|
||||
|
||||
Knowledge の新規作成 / 更新では、Phase 2 全体の原則に加えて以下を明示する:
|
||||
Knowledge の新規作成 / 更新では、consolidation 全体の原則に加えて以下を明示する:
|
||||
|
||||
- 採択ラインは「このプロジェクト / ユーザーに対して再度参照価値のある事実・ルール・ノウハウ」に限る
|
||||
- 一回限りの判断や議論は Decisions に留め、繰り返し参照される抽象化だけを Knowledge に上げる
|
||||
|
|
@ -87,7 +87,7 @@ Knowledge の新規作成 / 更新では、Phase 2 全体の原則に加えて
|
|||
|
||||
### 監査 LLM prompt
|
||||
|
||||
初期範囲では専用の監査 LLM は持たない(`memory.md` §書き込み経路と Linter / §将来検討 参照)。意味破壊の抑制は Phase 2 prompt 側の情報損失最小化指示と git diff レビューに寄せる。後から 2 層目として挟む際の入力・check 項目・pass-fail 返却形式はそのときに詰める。
|
||||
初期範囲では専用の監査 LLM は持たない(`memory.md` §書き込み経路と Linter / §将来検討 参照)。意味破壊の抑制は consolidation prompt 側の情報損失最小化指示と git diff レビューに寄せる。後から 2 層目として挟む際の入力・check 項目・pass-fail 返却形式はそのときに詰める。
|
||||
|
||||
## 関連
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Workflow(`/<slug>` で呼び出される制約付き作業フロー)は別 p
|
|||
- **同一 slug の衝突は新規作成禁止**。既存があれば update(Linter で検証、sub-Worker は read→edit に切り替え)
|
||||
- 同主題の content 進化 = 上書き update + git log で履歴追跡
|
||||
- 別主題が古い主題を置き換える場合のみ、別 slug で新規作成し古い方に `status: replaced` + `replaced_by: <新 slug>` を記録
|
||||
- Phase 1 の中間ストアとして `memory/_staging/<id>.json` を使う(短命、P2 完了で cleanup。ここは衝突回避と順序のため UUIDv7 可)
|
||||
- extract の中間ストアとして `memory/_staging/<id>.json` を使う(短命、consolidation 完了で cleanup。ここは衝突回避と順序のため UUIDv7 可)
|
||||
- Raw session log は既存 `session-store` で保持する。memory 対象外、参照経路のみ
|
||||
|
||||
### Knowledge の呼び出し制御
|
||||
|
|
@ -40,11 +40,11 @@ agentskills.io の `SKILL.md` 形式は採用しない。Knowledge は `#<slug>`
|
|||
| `model_invokation` | description が model context に載り、モデルが自発的に参照判断できる | **OFF** |
|
||||
| `user_invocable` | ユーザーが `#<slug>` で明示的に呼べる | **ON** |
|
||||
|
||||
Knowledge は Phase 2 が自律的に新規作成 / 更新 / フラグ切替を行う前提。毎回の人間承認 gate は設けない(実効性が低い)。保護は 3 段で担保:
|
||||
Knowledge は consolidation が自律的に新規作成 / 更新 / フラグ切替を行う前提。毎回の人間承認 gate は設けない(実効性が低い)。保護は 3 段で担保:
|
||||
|
||||
- **採択 gate**: Knowledge 新規作成は使用頻度メトリクスの Knowledge 化候補レポート(後述)に載った source から派生する場合に限る。閾値未満のうちは decisions / requests に留める
|
||||
- **Linter**: 構造違反を watch(詳細は後述)。意味破壊の自動検出は初期は持たず、挙動を見てから監査 LLM 層を追加する(将来検討)
|
||||
- **OS ファイル権限**: 人間が書き換えさせたくない record は `-r--` にしてロック。Phase 2 の write は OS レベルで弾かれる
|
||||
- **OS ファイル権限**: 人間が書き換えさせたくない record は `-r--` にしてロック。consolidation の write は OS レベルで弾かれる
|
||||
|
||||
Workflow も同じフラグ仕様(`workflow.md` 参照)。per-record 保護フラグを提供する拡張は将来検討、初期は OS 権限で足りる。
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ Workflow も同じフラグ仕様(`workflow.md` 参照)。per-record 保護
|
|||
|
||||
Knowledge / memory を LLM に渡す経路は以下で固定。採択基準(次節)と表裏で、引ける前提がないと採択しても無意味になる。
|
||||
|
||||
- **Knowledge 検索ツール**: frontmatter 含めた全文検索。通常 Pod と Phase 2 Pod の両方に渡す
|
||||
- **Knowledge 検索ツール**: frontmatter 含めた全文検索。通常 Pod と consolidation Pod の両方に渡す
|
||||
- Input: `query`(自由文字列)。オプションで `slug`(完全一致 1 件返し、`#<slug>` 解決に使う)、`kind` filter
|
||||
- Output: `{ slug, kind, description, model_invokation, excerpt }` の配列。`excerpt` はマッチ箇所の前後数行
|
||||
- 対象は `knowledge/*.md`。派生 index ファイルは持たず実ファイルを都度スキャン
|
||||
|
|
@ -60,10 +60,10 @@ Knowledge / memory を LLM に渡す経路は以下で固定。採択基準(
|
|||
- ヒット件数上限と excerpt 行数は設定で tune
|
||||
- **memory 検索ツール**: `memory/{summary,decisions,requests}/*.md` 対象。spec は Knowledge 検索ツールと同型。§使用頻度メトリクスの観測経路と同一視する
|
||||
- **更新は memory 専用 Tool + Linter**: Knowledge / memory への write/edit は `memory` クレートが提供する専用 Tool 経由のみ。汎用 CRUD(`tools` クレートの Write/Edit)は memory/knowledge 配下を触らない(Pod が Scope で deny)。Linter は memory tool 内で pre-write 検証として走り、違反は `ToolError::InvalidArgument` で LLM に返る。詳細は §書き込み経路と Linter
|
||||
- **常駐注入**: メモリを消費する主体は通常 Pod。`model_invokation: ON` な record の description を通常 Pod の system prompt に常駐注入する。Phase 2 prompt には入れない
|
||||
- **常駐注入**: メモリを消費する主体は通常 Pod。`model_invokation: ON` な record の description を通常 Pod の system prompt に常駐注入する。consolidation prompt には入れない
|
||||
- 予算はシステムプロンプト全体の予算に含める(`memory_summary.md` の 5k 枠とは別管理にしない)
|
||||
- 超過時の件数キャップ / 優先順位ルールは、description 1024 chars 上限で通常は収まる前提。ON record 数が増えたら追加する
|
||||
- **Phase 2 の Knowledge アクセス**: 全 Knowledge 本文を prompt に埋めず、Knowledge 検索ツール + memory 専用 Tool を agent に渡して自律探索させる(詳細は §Phase 2)
|
||||
- **consolidation の Knowledge アクセス**: 全 Knowledge 本文を prompt に埋めず、Knowledge 検索ツール + memory 専用 Tool を agent に渡して自律探索させる(詳細は §Consolidation)
|
||||
- **`#<slug>` 補完 / 自動呼び出し(大枠のみ、実装は段階的)**:
|
||||
- `#<slug>` は検索ツールの slug 完全一致経路で本文が展開される
|
||||
- 補完 UI(slug サジェスト)は TUI 側。`user_invocable: false` は候補除外
|
||||
|
|
@ -88,7 +88,7 @@ Linter は memory tool 内で **pre-write 検証** として走り、違反は `
|
|||
|
||||
人間編集(エディタ / git commit)は tool 層を経由しないため Linter を通らない。`memory::Linter` は pure 関数として export し、CLI / pre-commit hook 経路を後で別チケットで用意する。
|
||||
|
||||
意味破壊(rewrite で既存の主張・根拠が落ちる、Knowledge の記述主題がズレる等)の自動検出は初期範囲に含めない。Phase 2 prompt 側の情報損失最小化指示と git diff レビューで運用し、実使用で顕在化したら監査 LLM 層を後から挟む(将来検討)。
|
||||
意味破壊(rewrite で既存の主張・根拠が落ちる、Knowledge の記述主題がズレる等)の自動検出は初期範囲に含めない。consolidation prompt 側の情報損失最小化指示と git diff レビューで運用し、実使用で顕在化したら監査 LLM 層を後から挟む(将来検討)。
|
||||
|
||||
Linter ルールは 2 系統:
|
||||
|
||||
|
|
@ -114,16 +114,16 @@ Linter ルールは 2 系統:
|
|||
|
||||
Workflow 保護は専用 tool schema のトリックではなく Linter ルールで担保するため、人間が規則を読んで理解できる。OS ファイル権限や Scope 上の特別な保護機構は設けず、`memory/` 配下を write allow する以上の細工はしない。衝突は git で解決する前提。
|
||||
|
||||
### 自動化(consolidation)メカニズム
|
||||
### 自動化(extract / consolidation)メカニズム
|
||||
|
||||
**2 フェーズ構成**。Phase 1 は頻繁に発火して活動を raw event として抽出、Phase 2 は蓄積時のみ発火して永続化形式に統合する。参考: Codex Memories の Phase 1/2 構造。
|
||||
**extract / consolidation の 2 段構成**。extract は頻繁に発火して活動を raw event として抽出、consolidation は蓄積時のみ発火して永続化形式に統合する。参考: Codex Memories の extract/consolidation 構造。
|
||||
|
||||
#### Phase 1: 活動抽出
|
||||
#### Extract: 活動抽出
|
||||
|
||||
- **Trigger**: activity tokens の累積閾値(cumulative input tokens since last pointer)。tool call カウントは不採用(ツールカスタマイズ非依存・大小重みづけのため)
|
||||
- **実行主体**: 既存 compact と同じ Worker spawn 機構を再利用。Pod は立てない
|
||||
- **入力**: 前回 Phase 1 以降の session log 範囲。処理済み境界の pointer は session log 側に保持し、寿命を session と揃える。session-store のドメイン純度を保つため、汎用拡張点 `LogEntry::Extension { domain, payload }`(domain = `"memory.extract"`)に寄せ、session-store は memory ドメインを知らない
|
||||
- **出力**: JSON schema で**活動ログ**の候補配列を返す。Knowledge 等の派生物は Phase 2 が活動ログから導出するので、Phase 1 では純粋な「起きたこと」に絞る
|
||||
- **入力**: 前回 extract 以降の session log 範囲。処理済み境界の pointer は session log 側に保持し、寿命を session と揃える。session-store のドメイン純度を保つため、汎用拡張点 `LogEntry::Extension { domain, payload }`(domain = `"memory.extract"`)に寄せ、session-store は memory ドメインを知らない
|
||||
- **出力**: JSON schema で**活動ログ**の候補配列を返す。Knowledge 等の派生物は consolidation が活動ログから導出するので、extract では純粋な「起きたこと」に絞る
|
||||
- `decisions`: 判断したこと(選択肢 + 選んだ + 根拠)
|
||||
- `discussions`: 議論したこと(トピック + 論点)
|
||||
- `attempts`: 試したこと(試行 + 結果 + 成否)
|
||||
|
|
@ -132,58 +132,58 @@ Workflow 保護は専用 tool schema のトリックではなく Linter ルー
|
|||
- **書き込み先**: `memory/_staging/<id>.json`
|
||||
- LLM 出力(活動ログ JSON)は pod 側ラッパーが `source: { session_id, range: [start_entry, end_entry] }` を**機械付与**して wrap。LLM には source を推論させない
|
||||
- **モデル**: `memory.extract_model`。軽量だが文脈理解できる中堅クラス(Haiku / 4o-mini / Flash 相当)を想定
|
||||
- **Compact との順序**: 同一 turn 完了後の post-run チェックで Phase 1 を **compact より前** に走らせる。compact は history を組み替えるので、extract の入力範囲(session log 上の entry index)は compact 前のほうが安定する
|
||||
- **並走防止 (Phase 1 同士)**: Pod 上の `extract_in_flight` フラグで in-flight 中の新規 trigger を skip。完了時点で閾値超過していれば直ちに次回を発火し、新 pointer 以降の最大範囲を回収する(pending 状態は保持しない=完了時の閾値再評価で coalesce 相当の挙動を成立させる)
|
||||
- **Compact との順序**: 同一 turn 完了後の post-run チェックで extract を **compact より前** に走らせる。compact は history を組み替えるので、extract の入力範囲(session log 上の entry index)は compact 前のほうが安定する
|
||||
- **並走防止 (extract 同士)**: Pod 上の `extract_in_flight` フラグで in-flight 中の新規 trigger を skip。完了時点で閾値超過していれば直ちに次回を発火し、新 pointer 以降の最大範囲を回収する(pending 状態は保持しない=完了時の閾値再評価で coalesce 相当の挙動を成立させる)
|
||||
|
||||
#### Phase 2: 永続化への統合 + 整理
|
||||
#### Consolidation: 永続化への統合 + 整理
|
||||
|
||||
- **Trigger**: staging の累積ファイル数 or bytes が閾値超過
|
||||
- **実行主体**: Phase 1 を終えた pod が consolidation Worker を spawn。並走防止は staging 配下の進行状況ファイル(後述)で担保
|
||||
- **実行主体**: extract を終えた pod が consolidation Worker を spawn。並走防止は staging 配下の進行状況ファイル(後述)で担保
|
||||
- **入力**: 起動時スナップショットで確定した consumed ID list 分の staging エントリ(活動ログ + `source`)+ 既存 `memory/*`(summary / decisions / requests)の全文 + **Knowledge 化候補レポート**(後述の使用頻度メトリクスから機械集計、閾値超過の source 一覧)+ **整理材料**(明示 invoke の使用頻度メトリクス、Linter Warn、`replaced` chain、sources 過多情報)。既存 `knowledge/*` は全文を prompt に埋めず、Knowledge 検索ツール経由で agent が必要分を引く
|
||||
- **処理**: sub-Worker に **memory 専用 Tool(read / write / edit、Linter 内蔵)+ Knowledge 検索ツール + memory 検索ツール** を渡し、agentic に以下を自律判断:
|
||||
- **統合**: 新規 decisions / requests を 1 件 1 ファイルで追加。`sources` は staging の `source` をコピー(LLM 推論ではない)
|
||||
- 活動ログから派生する Knowledge(用語定義 / 運用方針 / ルール / 事実 / ノウハウ)を新規作成 or 既存 patch。**新規作成は候補レポート掲載の source から派生する場合に限る**。`kind` を frontmatter に持ち、`last_sources` を更新
|
||||
- summary を必要に応じて rewrite
|
||||
- **整理(余力 phase)**: 既存 record 群を §評価カテゴリ で評価し、保護閾値外の対象を drop / merge / split / trim / rewrite。Linter Warn で検出した類似 slug 乱立 / sources 過多 / `replaced` 滞留はここで収斂させる
|
||||
- **整理(余力 step)**: 既存 record 群を §評価カテゴリ で評価し、保護閾値外の対象を drop / merge / split / trim / rewrite。Linter Warn で検出した類似 slug 乱立 / sources 過多 / `replaced` 滞留はここで収斂させる
|
||||
- **書き込み先**: `memory/*` と `knowledge/*`。Workflow 禁止は Linter で担保(`workflow.md` 参照)
|
||||
- **完了処理**: consumed ID list の staging のみ cleanup(実行中に Phase 1 が追加した分は残す)。Phase 2 完了時に staging に新着があれば次を発火(Coalesce)
|
||||
- **完了処理**: consumed ID list の staging のみ cleanup(実行中に extract が追加した分は残す)。consolidation 完了時に staging に新着があれば次を発火(Coalesce)
|
||||
- **モデル**: `memory.consolidation_model`。reasoning 系
|
||||
|
||||
##### 並走防止
|
||||
|
||||
- 場所: staging 配下に 1 ファイル(名前・形式は未定)
|
||||
- 中身: 動作中の Pod 識別子 + **consumed ID list**(この Phase 2 run が起動時スナップショットで確定した staging エントリ ID の列)
|
||||
- 中身: 動作中の Pod 識別子 + **consumed ID list**(この consolidation run が起動時スナップショットで確定した staging エントリ ID の列)
|
||||
- 占有ルール: そのファイルが存在し、示された Pod が動作している間、そのプロセスが排他占有
|
||||
- 実行中に Phase 1 が staging に追加したエントリは触らず、次回 Phase 2(Coalesce)に委ねる
|
||||
- 実行中に extract が staging に追加したエントリは触らず、次回 consolidation(Coalesce)に委ねる
|
||||
- cleanup は consumed ID list のエントリのみ削除、追加分は残す
|
||||
- クラッシュ時は consumed ID list から処理途中を特定できる。重複作成は同一 slug update に自然収束
|
||||
- 占有の実現方法(pid 存在確認 / flock / 他)は未定
|
||||
|
||||
#### Phase 2 agent への原則
|
||||
#### consolidation agent への原則
|
||||
|
||||
`memory/` 配下は人間も git 経由で編集する。Phase 2 prompt で以下を明示:
|
||||
`memory/` 配下は人間も git 経由で編集する。consolidation prompt で以下を明示:
|
||||
|
||||
- **rewrite は許可**。既存内容と新規情報を統合・再構成して情報密度を上げることを優先。単純 append(追記で増やすだけ)は避ける
|
||||
- rewrite 時は**情報損失を最小化**する: 既存の主張・根拠・sources を保持。表現を整理・短縮しても、含まれている要素は落とさない
|
||||
- Decision の置き換えは `status: replaced` + `replaced_by: <slug>` で表現、直接削除しない
|
||||
- 整理 phase での drop は許可。ただし保護閾値(§判断ルール)超過 record は drop / 大幅圧縮の対象外。誤判定しやすいものは merge / trim を優先
|
||||
- 整理 step での drop は許可。ただし保護閾値(§判断ルール)超過 record は drop / 大幅圧縮の対象外。誤判定しやすいものは merge / trim を優先
|
||||
- 各 record の整理理由は `outdated | superseded | unused | noisy` の §評価カテゴリ で説明可能にし、git diff から読み取れる粒度の操作にする
|
||||
- Knowledge は既存 record 群の slug / description / kind / `model_invokation` を入口に適合先を探し、自然に統合できるなら新規 slug を増やさない
|
||||
- 人間編集は git diff で顕在化する前提。整合しない rewrite は避け、衝突時は git で解決
|
||||
|
||||
#### Offer 経路
|
||||
|
||||
Memory record の書き込みは Phase 2 が自律判断し、Offer は設けない(Knowledge 含む)。人間承認経路が必要なのは以下:
|
||||
Memory record の書き込みは consolidation が自律判断し、Offer は設けない(Knowledge 含む)。人間承認経路が必要なのは以下:
|
||||
|
||||
- Workflow 関連の offer(新規作成 / 改善 / `model_invokation` ON 化)は `workflow.md` 参照
|
||||
|
||||
#### Compact との関係
|
||||
|
||||
基本分離(memory は独立トリガー、compact は `input_tokens` 既存閾値のまま)。compact で失われる session log の raw は **Phase 1 が compact より前に走ることで staging に保全**される(§Phase 1 §Compact との順序 参照)。Phase 2 を compact に同期させる義務はなく、staging 累積閾値で独立に発火する。
|
||||
基本分離(memory は独立トリガー、compact は `input_tokens` 既存閾値のまま)。compact で失われる session log の raw は **extract が compact より前に走ることで staging に保全**される(§Extract §Compact との順序 参照)。consolidation を compact に同期させる義務はなく、staging 累積閾値で独立に発火する。
|
||||
|
||||
### 整理(GC 相当)の扱い
|
||||
|
||||
Phase 2 は rewrite 許可で情報統合寄りの働きをするが、それでも残る以下の課題は **Phase 2 の余力 phase で同じ agent が処理**する(独立 trigger / 独立 Agent は持たない):
|
||||
consolidation は rewrite 許可で情報統合寄りの働きをするが、それでも残る以下の課題は **consolidation の余力 step で同じ agent が処理**する(独立 trigger / 独立 Agent は持たない):
|
||||
|
||||
- 重要度の低い record が累積する
|
||||
- 類似 slug が乱立する(Linter Warn で検出したものをまとめて処理)
|
||||
|
|
@ -195,7 +195,7 @@ Phase 2 は rewrite 許可で情報統合寄りの働きをするが、それで
|
|||
|
||||
#### 操作粒度
|
||||
|
||||
整理 phase は Phase 2 統合 phase と同じ memory 専用 Tool(read / write / edit、内部で pre-write Linter)を使う。operation 粒度は自然にサポートされる(専用 API は用意しない):
|
||||
整理 step は consolidation 統合 step と同じ memory 専用 Tool(read / write / edit、内部で pre-write Linter)を使う。operation 粒度は自然にサポートされる(専用 API は用意しない):
|
||||
|
||||
- **ファイル単位**: 丸ごと drop、複数ファイルの merge、1 ファイルの分割(split)
|
||||
- **ファイル内の部分削除**: 本文の一部節・箇条を削除 or 圧縮。frontmatter の `sources` 古いエントリの trim も含む
|
||||
|
|
@ -227,12 +227,12 @@ Phase 2 は rewrite 許可で情報統合寄りの働きをするが、それで
|
|||
|
||||
**累積方式**(後集計アプローチ): 上記 invoke 記録に対して最大 10 回前の invoke から現在までの時系列窓でフィルタして集計する。
|
||||
|
||||
**Knowledge 化候補レポート**: Phase 2 統合 phase が入力に受け取る、Knowledge 新規作成 gate 用の機械集計。対象は `memory/*` 配下の record(Phase 1 成果物である decisions / requests / 既存 knowledge)で、明示 invoke 頻度が閾値超過のものを列挙する。spike 除外のため、同一 session 内の連続参照は 1 count に丸め、複数 session での再参照を要件とする。閾値の具体値は運用で調整、設定ファイルで tune。
|
||||
**Knowledge 化候補レポート**: consolidation 統合 step が入力に受け取る、Knowledge 新規作成 gate 用の機械集計。対象は `memory/*` 配下の record(extract 成果物である decisions / requests / 既存 knowledge)で、明示 invoke 頻度が閾値超過のものを列挙する。spike 除外のため、同一 session 内の連続参照は 1 count に丸め、複数 session での再参照を要件とする。閾値の具体値は運用で調整、設定ファイルで tune。
|
||||
|
||||
#### 判断ルール
|
||||
|
||||
- 保護閾値: **明示 invoke** の `frequency >= 1.0 invokes/Mtoken` の record は drop / 大幅圧縮の対象外(初期値 1.0、workspace 設定でカスタマイズ可)。`model_invokation` 注入による常駐は計数対象外(別指標として後段で参照)
|
||||
- 整理 phase の評価カテゴリは `outdated | superseded | unused | noisy` を使う。単一 record が複数カテゴリに該当してもよい
|
||||
- 整理 step の評価カテゴリは `outdated | superseded | unused | noisy` を使う。単一 record が複数カテゴリに該当してもよい
|
||||
|
||||
### ファイル形式
|
||||
|
||||
|
|
@ -245,7 +245,7 @@ Phase 2 は rewrite 許可で情報統合寄りの働きをするが、それで
|
|||
- Knowledge 固有: `kind`, `description`, `model_invokation`, `user_invocable`
|
||||
- Knowledge の保存先は `knowledge/<slug>.md`。`memory/` とは兄弟ディレクトリに分ける
|
||||
- Decisions 固有: `status: open | resolved | replaced`、置き換え時は `replaced_by: <slug>`
|
||||
- Phase 1 staging: `memory/_staging/<id>.json`(JSON、1 件 1 ファイル、Phase 2 完了で削除。短命なので UUIDv7 可)。pod 側ラッパーが `source` を機械付与して LLM 出力と wrap
|
||||
- extract staging: `memory/_staging/<id>.json`(JSON、1 件 1 ファイル、consolidation 完了で削除。短命なので UUIDv7 可)。pod 側ラッパーが `source` を機械付与して LLM 出力と wrap
|
||||
- Workflow の frontmatter は `workflow.md` 参照
|
||||
|
||||
## Scope 外
|
||||
|
|
@ -254,13 +254,13 @@ Phase 2 は rewrite 許可で情報統合寄りの働きをするが、それで
|
|||
|
||||
### 将来検討(運用で必要性が見えたら追加)
|
||||
|
||||
- 監査 LLM 層(意味破壊検出)の導入 — 初期は静的 Linter のみで運用し、Phase 2 の rewrite で情報損失・主題ズレが実運用で顕在化したら memory tool 内の検証パイプラインに 2 層目として追加。入力 / check 項目 / pass-fail 返却形式は導入時に詰める
|
||||
- 監査 LLM 層(意味破壊検出)の導入 — 初期は静的 Linter のみで運用し、consolidation の rewrite で情報損失・主題ズレが実運用で顕在化したら memory tool 内の検証パイプラインに 2 層目として追加。入力 / check 項目 / pass-fail 返却形式は導入時に詰める
|
||||
- Vector index / FTS5 等の検索索引 — 初期は grep で足りる想定。ファイル数増加で検索が重くなったら検討
|
||||
- `model_invokation` offer の自動判定ロジック — 初期は人間が手動で切り替え
|
||||
- 過去 session を cross-session で検索する UI
|
||||
- Phase 2 を担う常駐 daemon 化 — オンデマンド + lock 方式で始める。必要性が出たら upgrade path として daemon 化
|
||||
- Deterministic promotion(OpenClaw 型 scoring + ゲート)— 初期は Phase 2 agent の LLM 判断に委ねる。運用実績で出力を評価してから、成熟カテゴリから scoring 導入
|
||||
- Shallow request の自動除外判定 — 初期は Phase 1 prompt で「些細な質問は返さなくてよい」と指示する程度。精緻な filter は後
|
||||
- consolidation を担う常駐 daemon 化 — オンデマンド + lock 方式で始める。必要性が出たら upgrade path として daemon 化
|
||||
- Deterministic promotion(OpenClaw 型 scoring + ゲート)— 初期は consolidation agent の LLM 判断に委ねる。運用実績で出力を評価してから、成熟カテゴリから scoring 導入
|
||||
- Shallow request の自動除外判定 — 初期は extract prompt で「些細な質問は返さなくてよい」と指示する程度。精緻な filter は後
|
||||
|
||||
### 別 plan / ticket で扱う
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Workflow は制約付きの強制的な作業フロー。`/<slug>` で明示的
|
|||
|
||||
Workflow は**人間が書く**、または consolidation が offer して人間が承認する。自動書き込みは禁止:
|
||||
|
||||
- consolidation Phase 2(`memory.md` 参照)の write tool schema に `workflow` カテゴリを含めないことで構造的に担保
|
||||
- consolidation(`memory.md` 参照)の write tool schema に `workflow` カテゴリを含めないことで構造的に担保
|
||||
- 新規作成 / 手順追加・更新は `Event::Notification` で提案し、人間承認で反映
|
||||
|
||||
### Offer 契機
|
||||
|
|
@ -58,4 +58,4 @@ consolidation が以下を検出した場合、Client に Notification を投げ
|
|||
|
||||
## 関連
|
||||
|
||||
- `memory.md`: Knowledge 定義、consolidation Phase 1/2、Offer の配送経路
|
||||
- `memory.md`: Knowledge 定義、extract/consolidation、Offer の配送経路
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ compact_request_threshold = 90000
|
|||
compact_retained_tokens = 8000
|
||||
compact_auto_read_budget = 8000
|
||||
compact_worker_max_input_tokens = 50000
|
||||
compact_worker_max_turns = 20
|
||||
|
||||
[compaction.model]
|
||||
scheme = "gemini"
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@
|
|||
|
||||
## 1. OpenAI Codex — Memories & Chronicle
|
||||
|
||||
Codex CLI に 2026-03 頃から追加された "Memories" 機能と、2026-04-15 頃に macOS 向け研究プレビューとして乗った "Chronicle"。スクリーン内容まで観測対象に広げた点が目新しいが、**コアは素朴な 2 フェーズの要約パイプライン**で、実装の示唆が多い。
|
||||
Codex CLI に 2026-03 頃から追加された "Memories" 機能と、2026-04-15 頃に macOS 向け研究プレビューとして乗った "Chronicle"。スクリーン内容まで観測対象に広げた点が目新しいが、**コアは素朴な extract / consolidation の 2 段パイプライン**で、実装の示唆が多い。
|
||||
|
||||
### データフロー(memories 本体)
|
||||
|
||||
セッション開始時に走る 2-phase パイプライン。`codex-rs/core/src/memories/` を直接読み確認した挙動:
|
||||
セッション開始時に走る 2-step パイプライン。`codex-rs/core/src/memories/` を直接読み確認した挙動:
|
||||
|
||||
- **Phase 1 — Extraction**(`phase1.rs`): 対象 rollout ごとにモデル呼び出しで **JSON schema 強制**の `StageOneOutput { raw_memory, rollout_summary, rollout_slug }` を返させる(`#[serde(deny_unknown_fields)]`)。`CONCURRENCY_LIMIT = 8` で並列実行、結果は SQLite の `stage1_outputs` テーブルに格納。`StateRuntime` の job leasing で duplicate work 防止
|
||||
- **extract — Extraction**(`extraction implementation`): 対象 rollout ごとにモデル呼び出しで **JSON schema 強制**の `StageOneOutput { raw_memory, rollout_summary, rollout_slug }` を返させる(`#[serde(deny_unknown_fields)]`)。`CONCURRENCY_LIMIT = 8` で並列実行、結果は SQLite の `stage1_outputs` テーブルに格納。`StateRuntime` の job leasing で duplicate work 防止
|
||||
- モデル: `gpt-5.4-mini` / Low reasoning(`memories.extract_model` で override 可)
|
||||
- テンプレ: `codex-rs/core/templates/memories/stage_one_system.md` + `stage_one_input.md`
|
||||
- **Phase 2 — Consolidation**(`phase2.rs`): **singleton** として走る(`jobs` テーブルで `kind = 'memory_consolidate_global'`, `job_key = 'global'` を `wx` 相当に claim)。`Phase2InputSelection` で前回 baseline との **added / retained / removed** 差分を計算し、その差分を prompt に埋めて sub-agent に投入
|
||||
- **consolidation — Consolidation**(`consolidation implementation`): **singleton** として走る(`jobs` テーブルで `kind = 'memory_consolidate_global'`, `job_key = 'global'` を `wx` 相当に claim)。`ConsolidationInputSelection` で前回 baseline との **added / retained / removed** 差分を計算し、その差分を prompt に埋めて sub-agent に投入
|
||||
- 出力は **自由形式 Markdown**(JSON schema なし)。sub-agent が memory_root を cwd に持ち、`MEMORY.md` / `memory_summary.md` / `skills/<name>/` を直接書き換える
|
||||
- モデル: `gpt-5.4` / Medium reasoning(`memories.consolidation_model` で override 可)
|
||||
- Heartbeat 90s / lease 3600s、失敗時 `retry_remaining` デフォルト 3
|
||||
|
|
@ -25,7 +25,7 @@ Codex CLI に 2026-03 頃から追加された "Memories" 機能と、2026-04-15
|
|||
- 生成タイミングは「スレッドが十分アイドルになってから」(age + idle window で判定)
|
||||
- `memory_summary.md` が **5,000 tokens cap** で system prompt に注入される(`MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_SUMMARY_TOKEN_LIMIT`)
|
||||
|
||||
**非対称の肝**: 抽出 (Phase 1) は structured output で分類を強制、統合 (Phase 2) は sub-agent に自由書き込みさせる。分類ブレを Phase 1 で封じ、統合の柔軟性は Phase 2 の agentic 判断に委ねる、という役割分担。insomnia の 2 フェーズ設計の直接の元ネタ。
|
||||
**非対称の肝**: 抽出 (extract) は structured output で分類を強制、統合 (consolidation) は sub-agent に自由書き込みさせる。分類ブレを extract で封じ、統合の柔軟性は consolidation の agentic 判断に委ねる、という役割分担。insomnia の extract / consolidation 設計の直接の元ネタ。
|
||||
|
||||
### 保存場所 / 形式
|
||||
|
||||
|
|
@ -33,13 +33,13 @@ Codex CLI に 2026-03 頃から追加された "Memories" 機能と、2026-04-15
|
|||
- `memory_summary.md` を中心に「summaries / durable entries / recent inputs / supporting evidence」の Markdown を束ねる構成。
|
||||
- Chronicle 派生メモリは `$CODEX_HOME/memories_extensions/chronicle/` に分離。
|
||||
- スクリーンキャプチャ中間物は `$TMPDIR/chronicle/screen_recording/` に置かれ、running 中 6h で自動削除。サーバー側には保存しない(法的義務時を除く)。
|
||||
- **Extension resource retention は決定論的 7 日 hard-coded**(`EXTENSION_RESOURCE_RETENTION_DAYS = 7`, `memories/extensions.rs:11`)。filename embedded timestamp (`%Y-%m-%dT%H-%M-%S`) が cutoff より古い `.md` リソースを Phase 2 直前に物理削除し、削除リストを `removed_extension_resources` として consolidation prompt に渡す(agent はそれを見て派生メモリを抹消)。
|
||||
- **Extension resource retention は決定論的 7 日 hard-coded**(`EXTENSION_RESOURCE_RETENTION_DAYS = 7`, `memories/extensions.rs:11`)。filename embedded timestamp (`%Y-%m-%dT%H-%M-%S`) が cutoff より古い `.md` リソースを consolidation 直前に物理削除し、削除リストを `removed_extension_resources` として consolidation prompt に渡す(agent はそれを見て派生メモリを抹消)。
|
||||
|
||||
### 設定
|
||||
|
||||
- `memories.generate_memories` / `memories.use_memories`: 生成 / 注入の on-off。
|
||||
- `memories.extract_model`: Phase 1 に使う軽量モデル。
|
||||
- `memories.consolidation_model`: Phase 2 のマージ側モデル(既定で reasoning 系)。
|
||||
- `memories.extract_model`: extract に使う軽量モデル。
|
||||
- `memories.consolidation_model`: consolidation のマージ側モデル(既定で reasoning 系)。
|
||||
- セッション内 `/memories` コマンドでスレッド単位に無効化可能。
|
||||
- シークレットは生成時に自動 redaction。
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ Self-improving agent を名乗るフレーム。メモリ周りは **3 層 + ク
|
|||
> Review the conversation above and consider saving or updating a skill if appropriate.
|
||||
> Focus on: was a non-trivial approach used to complete a task that required trial and error...
|
||||
> **If nothing is worth saving, just say 'Nothing to save.' and stop.**
|
||||
- 末尾の "Nothing to save." 指示が肝。頻繁発火でも中身ゼロの場合は NOP で抜ける設計。insomnia Phase 1 の「空配列許容」の直接ソース
|
||||
- 末尾の "Nothing to save." 指示が肝。頻繁発火でも中身ゼロの場合は NOP で抜ける設計。insomnia extract の「空配列許容」の直接ソース
|
||||
|
||||
### 書き込み機構
|
||||
|
||||
|
|
@ -252,10 +252,10 @@ Agent Workspace(`~/.openclaw/workspace/`)構成:
|
|||
メモリ更新のタイミング(`extensions/memory-core/src/` 実装より):
|
||||
|
||||
- **Compaction 前の silent turn**: `before_agent_reply` フックで `DREAMING_SYSTEM_EVENT_TEXT` を検出、`runShortTermDreamingPromotionIfTriggered` が発火。エージェントへの write-back 指示ではなく、**後述の dreaming pass をインラインで走らせる**仕組み
|
||||
- **Dreaming pass (optional)**: 実体は **3 phase** (Light + Deep + REM)、cron default `"0 3 * * *"` UTC
|
||||
- **Dreaming pass (optional)**: 実体は **3 passes** (Light + Deep + REM)、cron default `"0 3 * * *"` UTC
|
||||
- **Light**: 最近の recall / daily / session signals を short-term store に ingest + narrative 生成(**subagent LLM call 1 回**、`NARRATIVE_SYSTEM_PROMPT` で poetic な dream diary)
|
||||
- **Deep**: `applyShortTermPromotions()` が promotion を決定。**LLM call ゼロ**、以下の 6 重みスコアと 3 ゲートで機械判断
|
||||
- 重み: frequency (0.24) / relevance (0.30) / diversity (0.15) / recency (0.15) / consolidation (0.10) / conceptual (0.06) + phase boost
|
||||
- 重み: frequency (0.24) / relevance (0.30) / diversity (0.15) / recency (0.15) / consolidation (0.10) / conceptual (0.06) + pass boost
|
||||
- ゲート: `minScore 0.8` / `minRecallCount 3` / `minUniqueQueries 3`
|
||||
- **REM**: テーマ反映 + narrative LLM call 1 回
|
||||
- promotion 通過項目のみ `MEMORY.md` に append、`DREAMS.md` に人間レビュー用 diary
|
||||
|
|
@ -263,7 +263,7 @@ Agent Workspace(`~/.openclaw/workspace/`)構成:
|
|||
- **Lock**: `memory/.dreams/short-term-promotion.lock` を `wx` フラグで exclusive create、60s stale 検出 + 10s wait timeout、in-process の Map も併用
|
||||
- **モデルが "覚えている" のはディスクに書かれた内容だけ**、という明示ポリシー。隠れた state 無し
|
||||
|
||||
**insomnia にとって重要**: consolidation を LLM 依存から切り離せる見本。narrative は subagent が生成するが、promotion の判断は純機械(scoring)。insomnia の plan では Scope 外(Phase 2 は当面 agent 委任)だが、成熟したカテゴリから決定論的 promotion に差し替える upgrade path の参考になる。
|
||||
**insomnia にとって重要**: consolidation を LLM 依存から切り離せる見本。narrative は subagent が生成するが、promotion の判断は純機械(scoring)。insomnia の plan では Scope 外(consolidation は当面 agent 委任)だが、成熟したカテゴリから決定論的 promotion に差し替える upgrade path の参考になる。
|
||||
|
||||
**GC 観点の追加詳細**(`extensions/memory-core/src/short-term-promotion.ts:1518-1652` 実装より):
|
||||
|
||||
|
|
@ -464,7 +464,7 @@ insomnia で意思決定すべきポイントはこの対応表:
|
|||
|
||||
## 8. GC 機構の横断比較
|
||||
|
||||
`docs/plan/memory.md` §GC は「Phase 2 とは別経路で memory を再評価し、drop / merge / split / `replaced` chain 整理を行う」ことを決めた段階で、判断主体と処理種別の仕様をこれから詰める。本節は他プロジェクトの GC 設計を共通の 6 軸で並べて、insomnia で採るべき型の材料とする。
|
||||
`docs/plan/memory.md` §GC は「consolidation とは別経路で memory を再評価し、drop / merge / split / `replaced` chain 整理を行う」ことを決めた段階で、判断主体と処理種別の仕様をこれから詰める。本節は他プロジェクトの GC 設計を共通の 6 軸で並べて、insomnia で採るべき型の材料とする。
|
||||
|
||||
### 8.1 比較表
|
||||
|
||||
|
|
@ -472,9 +472,9 @@ insomnia で意思決定すべきポイントはこの対応表:
|
|||
|
||||
| # | プロジェクト / 機構 | 対象 | トリガー | 判断主体 | 処理種別 | 人間介入点 | 履歴保持 |
|
||||
|---|---------------------|------|----------|----------|----------|-----------|---------|
|
||||
| 1 | Codex Phase 2 consolidation | `MEMORY.md` block / `memory_summary.md` / `skills/*` | session idle + age | **LLM agentic**(sub-agent) | drop / merge / split / rewrite、removed thread id に紐づく block を **部分削除**(block 丸ごとは消さない、thread-local 記述のみ除去) | 無し(`/memories` で thread 単位 opt-out のみ) | git 任意、memory_root は単なる Markdown |
|
||||
| 2 | Codex extension resource retention | `memories_extensions/<name>/resources/*.md` | Phase 2 実行直前に cron 相当 | **決定論** | **物理削除** (filename timestamp cutoff) | 無し | 完全消去、Phase 2 prompt に removed list を通知 |
|
||||
| 3 | Codex stage1 pruning | `stage1_outputs` SQLite row | Phase 2 後 / 容量超過 | **決定論** | `selected_for_phase2 = 0` の古い row を cutoff + `LIMIT` で DELETE | 無し | SQL 完全削除 |
|
||||
| 1 | Codex consolidation | `MEMORY.md` block / `memory_summary.md` / `skills/*` | session idle + age | **LLM agentic**(sub-agent) | drop / merge / split / rewrite、removed thread id に紐づく block を **部分削除**(block 丸ごとは消さない、thread-local 記述のみ除去) | 無し(`/memories` で thread 単位 opt-out のみ) | git 任意、memory_root は単なる Markdown |
|
||||
| 2 | Codex extension resource retention | `memories_extensions/<name>/resources/*.md` | consolidation 実行直前に cron 相当 | **決定論** | **物理削除** (filename timestamp cutoff) | 無し | 完全消去、consolidation prompt に removed list を通知 |
|
||||
| 3 | Codex stage1 pruning | `stage1_outputs` SQLite row | consolidation 後 / 容量超過 | **決定論** | `selected_for_consolidation = 0` の古い row を cutoff + `LIMIT` で DELETE | 無し | SQL 完全削除 |
|
||||
| 4 | Hermes `memory` tool | `MEMORY.md` / `USER.md` のエントリ | **char limit (2,200 / 1,375) 超過時の add 拒否** | **LLM agentic**(エラー受けて自分で `replace` / `remove` を呼ぶ) | drop / rewrite | 無し(all-agent) | ディスク消去(file lock で tx 化) |
|
||||
| 5 | Hermes background review | entry / skill | turn / iter カウンタ(10 デフォルト) | **LLM agentic**(fork agent、max_iterations = 8) | add / update / delete をレビュー判断、`Nothing to save.` で no-op | 無し | same as 4 |
|
||||
| 6 | OpenClaw `applyShortTermPromotions` | promotion candidate → `MEMORY.md` | Deep dreaming phase | **決定論**(6 重み合算 + 3 ゲート、LLM ゼロ) | **append のみ**(`<!-- openclaw-memory-promotion:<hash> -->` marker、既存 block は触らない) | 無し | 追記のみ、削除系統は別 |
|
||||
|
|
@ -497,23 +497,23 @@ insomnia で意思決定すべきポイントはこの対応表:
|
|||
**トリガー(いつ GC するか)の 4 パターン**:
|
||||
|
||||
1. **容量超過 hard reject**(Hermes): 追加要求を失敗で返して LLM に自律対処を強制。**決定論的 trigger + agentic 処理**で、設計最小コスト。
|
||||
2. **session idle + age**(Codex Phase 2): 人間の活動終了を待って非同期、最もユーザー体感を壊しにくい。
|
||||
2. **session idle + age**(Codex consolidation): 人間の活動終了を待って非同期、最もユーザー体感を壊しにくい。
|
||||
3. **cron / scheduled sweep**(OpenClaw dreaming default `0 3 * * *`, Codex extension retention): 定期的・予測可能。人間 review との組み合わせがしやすい。
|
||||
4. **ingest 時の即時**(Cloudflare supersession): 書き込みの tx 内で完結、後続 GC 走査が要らない。topic key 設計が前提。
|
||||
|
||||
insomnia の plan は (2) Phase 2 で rewrite 許可を置きつつ、GC は (3) 方向で別経路という構造。これは Codex / OpenClaw の両方と整合する。
|
||||
insomnia の plan は (2) consolidation で rewrite 許可を置きつつ、GC は (3) 方向で別経路という構造。これは Codex / OpenClaw の両方と整合する。
|
||||
|
||||
**判断主体の 3 系統**:
|
||||
|
||||
- **決定論のみ**: Codex retention / stage1 pruning / OpenClaw temporal decay / Cloudflare supersession / lint 検出。条件がはっきりしているもの(age / key 一致 / 構造的 issue)は決定論が強い。
|
||||
- **決定論 scoring → 閾値 gate → 機械適用**: OpenClaw Deep promotion。LLM の揺れを除き、コストも LLM コールゼロ。ただし対象が append 側のみで、削除には使われていない。
|
||||
- **LLM agentic**: Codex Phase 2 / Hermes review / Letta sleep-time。判断の柔軟性(block 内部分削除、context 依存の merge)を LLM に委ねる。
|
||||
- **LLM agentic**: Codex consolidation / Hermes review / Letta sleep-time。判断の柔軟性(block 内部分削除、context 依存の merge)を LLM に委ねる。
|
||||
|
||||
`docs/plan/memory.md` は Phase 2 が LLM agentic、GC も暫定的に **LLM agentic + Linter Warn 併用**としている。完全に一致する事例は **Codex Phase 2 の consolidation prompt**(836 行)で、「removed thread id を `MEMORY.md` から部分削除し、blockに他の thread が残っている場合は split / rewrite して保持」という手続きを自然言語で指示している。**insomnia は Linter 側に警告カテゴリ(類似 slug / `replaced` 滞留 / sources 過多 / stale)を先に定義し、GC 実行の agent プロンプトはそれを入力にする**構造が素直。
|
||||
`docs/plan/memory.md` は consolidation が LLM agentic、GC も暫定的に **LLM agentic + Linter Warn 併用**としている。完全に一致する事例は **Codex consolidation の consolidation prompt**(836 行)で、「removed thread id を `MEMORY.md` から部分削除し、blockに他の thread が残っている場合は split / rewrite して保持」という手続きを自然言語で指示している。**insomnia は Linter 側に警告カテゴリ(類似 slug / `replaced` 滞留 / sources 過多 / stale)を先に定義し、GC 実行の agent プロンプトはそれを入力にする**構造が素直。
|
||||
|
||||
**処理種別の選択肢**:
|
||||
|
||||
- `drop / merge / split / rewrite` の組み合わせは Codex Phase 2 が最も自由度高く、Hermes もそれに近い(entry 粒度)。
|
||||
- `drop / merge / split / rewrite` の組み合わせは Codex consolidation が最も自由度高く、Hermes もそれに近い(entry 粒度)。
|
||||
- `replaced` chain の整理は **Cloudflare だけが自動で版チェーン維持**、他は LLM 任せ。insomnia は decision record に `replaced_by` を入れているので、Cloudflare 方式の forward pointer 概念を **人間可読な `replaced_by:` frontmatter** で既に踏襲している。GC 時に chain をどこまで短く畳むか(長大な `a → b → c → d` を `a → d` に圧縮するか)は未決定論点で、Cloudflare は圧縮せず chain を保持する設計。
|
||||
- **`split` は Codex だけが明示**。block 内に複数 thread id が混ざった場合に thread id 単位で分ける。insomnia の「1 件 1 ファイル」方針では split = ファイル分割となり、主題の粒度判断は GC agent に委ねる必要がある。
|
||||
|
||||
|
|
@ -523,7 +523,7 @@ insomnia の plan は (2) Phase 2 で rewrite 許可を置きつつ、GC は (3)
|
|||
- audit-first(issue を surface し、人間が決断): memory-wiki lint / OpenClaw dreaming-repair
|
||||
- high-stake 限定 gate: LinkedIn CMA
|
||||
|
||||
insomnia の plan は「人間 offer 承認を併用」なので **audit-first に寄る**のが自然。lint 相当の Warn を Linter で出し、LLM Phase 2 / GC がそれを消費する前に人間が承認 / 拒否できる UI を提供する構造。memory-wiki lint は `reports/lint.md` というシンプルな Markdown 出力なので、そのまま `memory/reports/gc-lint.md` 相当を tick off する実装が参考になる。
|
||||
insomnia の plan は「人間 offer 承認を併用」なので **audit-first に寄る**のが自然。lint 相当の Warn を Linter で出し、LLM consolidation / GC がそれを消費する前に人間が承認 / 拒否できる UI を提供する構造。memory-wiki lint は `reports/lint.md` というシンプルな Markdown 出力なので、そのまま `memory/reports/gc-lint.md` 相当を tick off する実装が参考になる。
|
||||
|
||||
**履歴保持の 3 モデル**:
|
||||
|
||||
|
|
@ -535,15 +535,15 @@ insomnia は **git 管理下に memory を置く**前提なので、物理削除
|
|||
|
||||
### 8.3 insomnia の GC 仕様を詰めるときの示唆
|
||||
|
||||
1. **GC trigger は 2 系統に割る**。(a) 決定論: Linter Warn 群 + age / count / size 閾値の sweep、(b) LLM 判定: Phase 2 とは別 prompt で Linter の issue リストを入力に渡す。両方が `memory/reports/gc-*.md` 相当を書き、それを次回の GC run が読む、というフィードバックループが OpenClaw lint / Codex Phase 2 input selection の両方と整合する。
|
||||
1. **GC trigger は 2 系統に割る**。(a) 決定論: Linter Warn 群 + age / count / size 閾値の sweep、(b) LLM 判定: consolidation とは別 prompt で Linter の issue リストを入力に渡す。両方が `memory/reports/gc-*.md` 相当を書き、それを次回の GC run が読む、というフィードバックループが OpenClaw lint / Codex consolidation input selection の両方と整合する。
|
||||
2. **Linter に「GC 候補検出」カテゴリを足す**。memory-wiki の lint issue code が参考になる: `stale-page`(90d 超)/ `stale-claim` / `low-confidence` / `orphan` / `duplicate-id` / `broken-wikilink` / `contradiction-present` / `open-question`。insomnia 固有の追加候補: `similar-slug`(類似 slug 乱立、既に plan に記載)/ `replaced-chain-long`(`replaced_by` が 3 段以上)/ `sources-overflow`(1 record の sources が閾値超)/ `knowledge-invoke-frequency-low`(`user_invoke` が一定期間ゼロ)。
|
||||
3. **処理は rewrite 優先、削除は `status: replaced` 経由**(既に plan 方針と一致)。forward pointer は Cloudflare 流、ただし chain 圧縮ルール(例: 「chain が n 段超えたら中間を drop、端のみ残す」)を決めるかは別論点。Cloudflare は圧縮しない、insomnia は git があるので圧縮してよい。
|
||||
4. **char limit は採用しない方が筋が良い**。Hermes の hard limit + LLM self-rewrite は設計最小だが、insomnia は 1 record 1 file なのでファイル内 size 制約は薄く、file 数による grep コストの方が支配的になる。file 数閾値 → GC trigger の方が insomnia の形に合う。
|
||||
5. **決定論 scoring を後から差し込む余地を残す**。OpenClaw Deep phase のような「頻度 / 関連度 / 多様性 / 時間減衰 / 整合性 / 概念」の 6 重み + 閾値は、agent LLM の出力が運用で評価可能になった段階で部分的に差し替える upgrade path として最適。初期は Phase 2 LLM + Linter Warn で十分。
|
||||
5. **決定論 scoring を後から差し込む余地を残す**。OpenClaw Deep pass のような「頻度 / 関連度 / 多様性 / 時間減衰 / 整合性 / 概念」の 6 重み + 閾値は、agent LLM の出力が運用で評価可能になった段階で部分的に差し替える upgrade path として最適。初期は consolidation LLM + Linter Warn で十分。
|
||||
6. **削除は git commit 単位で可逆**という前提を明示する。プロジェクトメモリは git 管理下なので、GC が誤って drop してもユーザーは revert できる。これは Codex が持っていない利点で、GC agent の判断を多少攻めても安全マージンがある。
|
||||
|
||||
一次ソース:
|
||||
- Codex Phase 2 consolidation: `~/ghq/github.com/openai/codex/codex-rs/core/src/memories/phase2.rs`, `core/templates/memories/consolidation.md`
|
||||
- Codex consolidation: `~/ghq/github.com/openai/codex/codex-rs/core/src/memories/consolidation implementation`, `core/templates/memories/consolidation.md`
|
||||
- Codex retention / stage1 pruning: `~/ghq/github.com/openai/codex/codex-rs/core/src/memories/extensions.rs:11-139`, `codex-rs/state/src/runtime/memories.rs:290-331, 333-464`
|
||||
- Hermes char limit reject: `~/ghq/github.com/NousResearch/hermes-agent/tools/memory_tool.py:211-266`
|
||||
- Hermes review spawn: `~/ghq/github.com/NousResearch/hermes-agent/run_agent.py:2727-2830`
|
||||
|
|
|
|||
|
|
@ -133,11 +133,11 @@ Insomnia の方が洗練されている(要約生成まで組み込み済み
|
|||
**取り込み案:**
|
||||
|
||||
```
|
||||
Phase 1: Prune(Hook ベース)
|
||||
extract: Prune(Hook ベース)
|
||||
PreLlmRequest Hook で古いツール出力を削除
|
||||
設計原則3に従い、新しい抽象は作らない
|
||||
|
||||
Phase 2: Compact(Agent ベース)
|
||||
consolidation: Compact(Agent ベース)
|
||||
OnTurnEnd Hook でトークン数をチェック
|
||||
閾値超過時に要約生成を挿入
|
||||
Workerのresume機構で作業を継続
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
You are the Phase 2 consolidation worker for an INSOMNIA memory subsystem.
|
||||
You are the consolidation worker for an INSOMNIA memory subsystem.
|
||||
|
||||
Your job is to take Phase 1 activity-log staging entries together with the workspace's current `memory/*` / `knowledge/*` records, then run two phases back-to-back in this single session:
|
||||
Your job is to take extract activity-log staging entries together with the workspace's current `memory/*` / `knowledge/*` records, then run two steps back-to-back in this single session:
|
||||
|
||||
1. **Consolidation phase** — fold staging into memory and knowledge.
|
||||
2. **Tidy phase** — clean up the existing records that the consolidation phase didn't already touch.
|
||||
1. **Integration step** — fold staging into memory and knowledge.
|
||||
2. **Tidy step** — clean up the existing records that the integration step didn't already touch.
|
||||
|
||||
You have:
|
||||
- `MemoryRead`, `MemoryWrite`, `MemoryEdit` for memory and knowledge records.
|
||||
|
|
@ -12,7 +12,7 @@ You have:
|
|||
|
||||
Your initial user message contains the staging entries, the full memory records, the knowledge candidate report, and the tidy hints. Existing knowledge bodies are NOT in the prompt; pull them through `KnowledgeQuery` + `MemoryRead` when relevant.
|
||||
|
||||
# Common rules (both phases)
|
||||
# Common rules (both steps)
|
||||
|
||||
- **Do not invent provenance.** Decisions / Requests `sources` arrays MUST be copied from the staging `source` field for the originating activity log entries. Do not synthesise `session_id` or entry ranges. Do not fabricate `last_sources` for Knowledge.
|
||||
- **Rewrite is allowed and often preferred over append.** When integrating new information, restructure existing records to raise information density. Preserve the existing claims, rationale, and `sources` while you compress.
|
||||
|
|
@ -24,7 +24,7 @@ Your initial user message contains the staging entries, the full memory records,
|
|||
- **Slug rules.** Slugs are kebab-case, short, recognisable, and must be unique within their kind. Same-slug create is a linter error — use Edit instead.
|
||||
- **Linter errors come back as tool errors.** When the memory linter rejects a write, read the error, fix the issue (missing frontmatter field, oversized body, unknown reference, etc.), and try again. Do not work around the rule.
|
||||
|
||||
# Consolidation phase
|
||||
# Integration step
|
||||
|
||||
Walk every staging entry in the input. For each one:
|
||||
|
||||
|
|
@ -37,9 +37,9 @@ Walk every staging entry in the input. For each one:
|
|||
- **Knowledge creation is gated.** Only create a new `knowledge/<slug>.md` when the originating source appears in the supplied "Knowledge candidate report". When the report is empty (the metrics pipeline is still being built), do not create new knowledge — fold the activity into decisions / requests / summary or update existing knowledge instead.
|
||||
- Rewrite `memory/summary.md` only when needed. Aim for 1–5k tokens. Preserve the high-level shape (current focus, recent decisions, stable facts) while pruning stale items.
|
||||
|
||||
# Tidy phase
|
||||
# Tidy step
|
||||
|
||||
Once the consolidation phase is done, evaluate every existing memory and knowledge record against four categories:
|
||||
Once the integration step is done, evaluate every existing memory and knowledge record against four categories:
|
||||
|
||||
- `outdated`: was correct, no longer matches the current implementation / policy / operation.
|
||||
- `superseded`: another record is now the de-facto authoritative one; this one is mostly redundant.
|
||||
|
|
@ -50,13 +50,13 @@ A single record may fall into more than one category. Choose one of `drop / merg
|
|||
|
||||
- Prefer `merge` and `trim` over `drop` for anything you'd flag as `unused` or `noisy` — git can reverse you, but a confidently-wrong drop hurts discovery.
|
||||
- `drop` is allowed for `outdated` / `superseded` records you can justify in the diff.
|
||||
- `replaced` markers (`status: replaced`) and chains pointed at by the tidy hints should be collapsed in this phase.
|
||||
- `replaced` markers (`status: replaced`) and chains pointed at by the tidy hints should be collapsed in this step.
|
||||
|
||||
**Protection threshold.** When the tidy hints include explicit-invoke metrics, records with `frequency >= 1.0 invokes/Mtoken` are off-limits to drop / large compression. The metrics pipeline is not always populated; when the input lacks frequency data, behave conservatively and skip drop on long-standing records.
|
||||
|
||||
# Closing the turn
|
||||
|
||||
When both phases are done, write a short final assistant message stating:
|
||||
When both steps are done, write a short final assistant message stating:
|
||||
|
||||
- which staging entries you folded in (by short summary, not by ID),
|
||||
- which existing records you touched (slug + operation),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
You are the Phase 1 activity extractor for an INSOMNIA memory subsystem.
|
||||
You are the activity extractor for an INSOMNIA memory subsystem.
|
||||
|
||||
Your single job: read the supplied conversation slice and emit a structured JSON record of "what happened" via the `write_extracted` tool. You are not consolidating, summarising, or generating knowledge — that is a later phase's job.
|
||||
Your single job: read the supplied conversation slice and emit a structured JSON record of "what happened" via the `write_extracted` tool. You are not consolidating, summarising, or generating knowledge — that is the consolidation worker's job.
|
||||
|
||||
# Hard rules
|
||||
|
||||
|
|
|
|||
56
tickets/compact-worker-occupancy-cap.md
Normal file
56
tickets/compact-worker-occupancy-cap.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Compact worker のサーキットブレーカーを占有量ベースに統一
|
||||
|
||||
## 背景
|
||||
|
||||
Compact worker のサーキットブレーカー (`crates/pod/src/compact/worker.rs:253-269` の `CompactWorkerInterceptor`) は `compact_worker_max_input_tokens` を `UsageEvent.input_tokens` の **累積和** で見ている。一方、`UsageEvent.input_tokens` の定義 (`crates/llm-worker/src/llm_client/event.rs:76-94`) は「送信した prompt prefix の総トークン数(占有量、キャッシュ込み)」であり、Anthropic 側でも `cache_read + cache_creation` を加算してこの規約に揃えている。
|
||||
|
||||
結果として現行の累積メトリックは、毎ターン同じ prefix をフルカウントする「context size × ターン数」相当の値を測っており:
|
||||
|
||||
- compact worker は `build_summary_prompt` (`crates/pod/src/pod.rs:2630-2657`) で reasoning / ToolCall.arguments / ToolResult.content を strip した skeleton + 探索ツールという設計なので、初回 input は元 history より大幅に小さい(数十 K 程度)。
|
||||
- それでも cache hit 含む prefix を毎ターン丸ごと足していくため、20-30K の skeleton を入力にツールを 2-3 回叩いた時点で 50K デフォルトに到達する。
|
||||
- prompt cache が 99% ヒットしていても累積値は同じだけ増えるので、コストの近似にも安全マージンの近似にもなっていない。
|
||||
|
||||
メイン Worker 側の対応するしきい値 (`compact_threshold`, `compact_request_threshold`) は `Pod::total_tokens()` (`crates/pod/src/pod.rs:146-149` → `llm_worker::token_counter::total_tokens(history, &usage_records)`) を見ており、これは `UsageRecord` 列を最新測定 + バイト按分で射影した「現在の占有量」(単一の値, 累積ではない)。Compact worker でもこの正規のカウンタに統一すべき。
|
||||
|
||||
サーキットブレーカーとして測るべき軸は二つあり、占有量カウンタは前者だけを担当する:
|
||||
|
||||
1. **占有量** (cost / window 圧迫の相当値): `total_tokens()` を流用。
|
||||
2. **ループ深さ** (短い context でツールを延々叩く暴走): `Worker::set_max_turns` (`crates/llm-worker/src/worker.rs:1369`) で別途上限を入れる。
|
||||
|
||||
## 方針
|
||||
|
||||
`CompactWorkerInterceptor` を、メインと同じ `UsageTracker` + `total_tokens` 機構に乗せ替える。累積メトリックは廃止する。ループ深さ対策として `compact_worker_max_turns` を新設し、`set_max_turns` 経由で compact worker に伝える。
|
||||
|
||||
## 要件
|
||||
|
||||
- `CompactWorkerInterceptor` を削除または書き換え、`pre_llm_request` の判定を `llm_worker::token_counter::total_tokens(worker.history(), &records).tokens > max_input_tokens` に切り替える。`input_so_far: AtomicU64` の累積パスは廃止。
|
||||
- Compact worker にも `UsageTracker` を持たせ、`pre_llm_request` で `note_request(history.len())`、`on_usage` で `record_usage` する。メイン Pod (`pod.rs:777-780`) と同じ配線パターン。
|
||||
- `compact_worker_max_input_tokens` の意味を「compact worker 側の現在占有量しきい値」に変更し、ドキュメントとデフォルト値を更新する。デフォルトは `compact_threshold` と単位が揃うため、現行 50K のままだと typical な main 側設定 (80K) に対して小さく compact 自身がそれを下回るのは妥当な範囲。実値は新セマンティクスで再評価する(要件としては「累積値ではなく占有量を測る」ことのみで固定)。
|
||||
- `CompactionConfig` に `compact_worker_max_turns: Option<u32>` を追加し、`compact()` (`pod.rs:1458-`) で `summary_worker.set_max_turns` に渡す。`None` のときは無制限(既存動作)。デフォルトは要検討(仮: `Some(20)`)。
|
||||
- 後方互換 shim は入れない。`compact_worker_max_input_tokens` はフィールド名を維持しつつセマンティクスだけ差し替えるため、旧設定値はそのまま新セマンティクスで解釈される。閾値のオーダーは大きくは変わらないので運用上の破壊的影響は小さい。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- 通常の compact 実行で、cache hit 込みの prefix がフルカウントされなくなり、`build_summary_prompt` の skeleton + 数回のファイル読み程度では cap に当たらない。
|
||||
- 短い context でツールを延々呼び続ける疑似ケースで、`compact_worker_max_turns` により compact run が打ち切られる。
|
||||
- `Pod::total_tokens()` と compact worker の占有量推定で同じ算出経路 (`llm_worker::token_counter::total_tokens`) が使われている。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- `compact_threshold` / `compact_request_threshold` 自体のセマンティクス・既定値変更。
|
||||
- Compact worker が更に compact をかける(meta-compact)。
|
||||
- `compact_auto_read_budget` ロジックの変更。
|
||||
- token 推定アルゴリズム自体の改善。
|
||||
|
||||
## 影響範囲
|
||||
|
||||
- `crates/pod/src/compact/worker.rs`: `CompactWorkerInterceptor` の実装、`input_so_far` 経路の削除。
|
||||
- `crates/pod/src/pod.rs`: `compact()` の compact worker 構築箇所 (`UsageTracker` 配線、`on_usage` 差し替え、`set_max_turns` 呼び出し)。
|
||||
- `crates/pod/src/compact/usage_tracker.rs`: 既存 `UsageTracker` を compact worker からも使うため、必要なら可視性 / API の調整。
|
||||
- `crates/manifest/src/lib.rs`, `crates/manifest/src/config.rs`, `crates/manifest/src/defaults.rs`: `compact_worker_max_input_tokens` のドキュメント更新、`compact_worker_max_turns` の追加とカスケード。
|
||||
- `crates/pod/src/compact/worker.rs` のユニットテスト、および compact 関連の統合テスト。
|
||||
|
||||
## Review
|
||||
- 状態: Approve with follow-up
|
||||
- レビュー詳細: [./compact-worker-occupancy-cap.review.md](./compact-worker-occupancy-cap.review.md)
|
||||
- 日付: 2026-05-11
|
||||
41
tickets/compact-worker-occupancy-cap.review.md
Normal file
41
tickets/compact-worker-occupancy-cap.review.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Review: Compact worker のサーキットブレーカーを占有量ベースに統一
|
||||
|
||||
レビュー対象: commit `ef0cdf7` (base `d818b37`).
|
||||
|
||||
## 前提・要件の確認
|
||||
|
||||
- 要件1「`CompactWorkerInterceptor` を `total_tokens` ベースに切り替え、`input_so_far: AtomicU64` 経路を廃止」: 満たされている。`crates/pod/src/compact/worker.rs:249-274` で `usage_tracker.records()` → `llm_worker::token_counter::total_tokens(context, &records)` に置き換わっており、`AtomicU64` import / フィールドも削除されている。`crates/pod/src/pod.rs:1456` 周辺の `use std::sync::atomic` も消えている。
|
||||
- 要件2「Compact worker に `UsageTracker` を持たせ、`pre_llm_request` で `note_request`、`on_usage` で `record_usage`」: 満たされている。`pod.rs:1537-1547` で `summary_usage_tracker = Arc::new(UsageTracker::new())` を作り、`on_usage` で `record_usage(event)`、`CompactWorkerInterceptor::pre_llm_request` 内で `usage_tracker.note_request(context.len())` を呼ぶ (`compact/worker.rs:262-272`)。
|
||||
- 要件3「`compact_worker_max_input_tokens` の意味を「現在占有量しきい値」に変更し doc 更新」: 満たされている。`crates/manifest/src/lib.rs:324`、`crates/manifest/src/defaults.rs:39-43`、`docs/compaction.md:143`、`docs/manifest.toml:215` がすべて「現在占有」「累計ではない」表現に揃っている。フィールド名は維持され、後方互換 shim は入っていない (要件通り)。
|
||||
- 要件4「`compact_worker_max_turns: Option<u32>` を新設し `Worker::set_max_turns` 経由で渡す」: 満たされている。`CompactionConfig` (`lib.rs:329-332`)、`CompactionConfigPartial` (`config.rs:118-119`)、`defaults::COMPACT_WORKER_MAX_TURNS = Some(20)` (`defaults.rs:46-48`)、`pod.rs:1548-1550` で `summary_worker.set_max_turns(...)`。manifest 側の merge / TryFrom / TOML パースもテスト付きで通っている (`config.rs:960-983`, `lib.rs:521-546`)。
|
||||
- 要件5「後方互換 shim 無し」: 満たされている。フィールド名は変更しないため旧 manifest はそのまま新セマンティクスで読まれ、deprecated alias 等は導入されていない。
|
||||
- 完了条件1「cache hit 込み prefix がフルカウントされない」: 単体テスト `compact_worker_interceptor_uses_occupancy_not_cumulative_usage` (`compact/worker.rs:301-328`) で「2 回 100-token 入力 → 累積 200 でも occupancy は最新の 100 のままで cap=150 を通る」を確認。
|
||||
- 完了条件2「`compact_worker_max_turns` で打ち切られる」: `set_max_turns` のロジック自体は llm-worker 側の既存挙動 (`worker.rs:1045-1050`) に乗るのみで、本チケットでは新規ロジックを足していない。配線テスト (manifest 側のパース) は入っているが、compact 経由で実 abort する pod-level の統合テストは無し。Trivial wiring なのでブロッキングではないが、後で run-level テストを足すと安心。
|
||||
- 完了条件3「`Pod::total_tokens()` と同じ算出経路」: 満たされている。`pod.rs:148` (`llm_worker::token_counter::total_tokens(self.history(), &usage)`) と `compact/worker.rs:265` (`llm_worker::token_counter::total_tokens(context, &records)`) が同じ関数を経由する。compact 側は per-request の prune 後 `request_context` を渡すため、メイン側と完全一致ではないが、`pre_llm_request` 時点でのその後 LLM に投げる prefix 占有量という意味では妥当 (むしろ正確)。
|
||||
|
||||
## アーキテクチャ・スコープ
|
||||
|
||||
- 修正は `compact/worker.rs` + `pod.rs` の compact 構築 + manifest 1 フィールド追加に閉じており、ticket 影響範囲表と完全一致。`crates/pod/src/compact/usage_tracker.rs` への変更は「`records()` メソッドを `pub(crate)` で追加して drain せず読む」のみで、API 拡張は最小。
|
||||
- `UsageTracker` は元々「per-LLM-request 計測のペアリング・drain は Pod が persist 時に行う」モデル。compact worker は drain せず read-only で snapshot を覗くだけなので、メインの persist セマンティクスを汚さない。compact worker のループ内で記録しても drain しないままワーカーが破棄される (`pod.rs:1530-1551` で関数ローカル) ため、メインの `Pod::usage_tracker` (turn 永続化用) とは完全に独立した別インスタンスで、相互干渉しない。設計として綺麗。
|
||||
- `note_request` をメインでは `UsageTrackingHook` (`pod.rs:46-59`) として `pre_llm_request` Hook で呼んでいるのに対し、compact 側は `CompactWorkerInterceptor::pre_llm_request` 内で直接呼んでいる。Worker のコール順 (Hook → Interceptor → stream → on_usage) を踏まえると `note_request` は `record_usage` より前に呼ばれていれば意味的に等価で、両者とも同じ tick で呼ばれるため不整合は無い。ただし「メイン: Hook で `note_request`、Interceptor で別判定」「compact: Interceptor で両方」と配線パターンが分岐している点は将来読む人が混乱するかも。今のままでも動くが、補足コメントを Pod 側か Interceptor 側に入れておくと親切。
|
||||
- ループ深さは `Worker::set_max_turns` 既存機構の流用。新規ロジック無し、コードベースを歪めない選択。
|
||||
|
||||
## 指摘事項
|
||||
|
||||
### Blocking
|
||||
無し。
|
||||
|
||||
### Non-blocking / Follow-up
|
||||
- `pod.rs:1548-1550` の `if compact_worker_max_turns.is_some() { summary_worker.set_max_turns(compact_worker_max_turns); }` は不要な分岐。`set_max_turns` は `Option<u32>` を取り、`None` を渡しても初期値 `None` を上書きするだけで no-op (`crates/llm-worker/src/worker.rs:1369-1371` + `worker.rs:1181`)。無条件に `summary_worker.set_max_turns(compact_worker_max_turns);` で十分。条件付けることで「`None` だと何もしない」読者バイアスを生んでむしろ読みづらい。
|
||||
- `crates/pod/src/pod.rs:2178-2201` の `MemoryExtractWorkerInterceptor` は古い累積メトリック方式のまま残っており、コメントには `Mirror of compact::worker::CompactWorkerInterceptor;` と書かれている。今回の変更で「mirror」ではなくなったのでコメントが嘘になっている。memory extract 側のセマンティクス変更は本チケットの範囲外で正しい判断だが、コメント補正 (例: 「以前は CompactWorkerInterceptor のミラーだったが、compact 側は占有量ベースに移行した。memory extract 側は別チケットで追従予定」) を別チケットなり TODO なりで残しておきたい。
|
||||
- `compact_worker_max_turns` が compact 経由で実際に `Aborted/MaxTurnsReached` を引き起こす経路の pod-level 統合テストは含まれていない。配線そのものはトリビアルで、`set_max_turns` の振る舞いは llm-worker 側で別途テストされている前提。後追いで足すかどうかは判断に任せる。
|
||||
- デフォルト `COMPACT_WORKER_MAX_TURNS = Some(20)` は `build_summary_prompt` の skeleton + 数回ファイル読みなら十分余裕がある妥当なライン。ただし auto_read_budget (8000) と read_file の典型呼び出しサイズを考えると、深いツールループが起きる前にトークン cap が先に効きやすい設計なので、20 が上限になる場面はまずレアケース。妥当な保険値として OK。
|
||||
|
||||
### Nits
|
||||
- `compact/worker.rs:249-253` の docstring は新しいセマンティクスを正しく説明できている。良。
|
||||
- `compact/usage_tracker.rs:102-112` `records()` の docstring が「request-time circuit breakers が同じ occupancy projection を見るため」と用途を明示しており、将来の extract worker 側追従の伏線にもなっている。良。
|
||||
- ユニットテスト `compact_worker_interceptor_uses_occupancy_not_cumulative_usage` (`compact/worker.rs:301-328`) は「累積では落ちる量でも occupancy では通る」ケースをピンポイントで掴んでいて、今回の本質的なバグへの回帰防止として的確。
|
||||
|
||||
## 判断
|
||||
|
||||
Approve with follow-up — チケットの 4 要件と完了条件 1/3 はすべて根拠つきで満たされている。残るのは (a) `set_max_turns` 呼び出しの不要な分岐、(b) `MemoryExtractWorkerInterceptor` 側のコメント陳腐化と将来の追従、(c) max_turns の pod-level 統合テスト追加、いずれも非ブロッキング。マージ可。
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
INSOMNIA が内部で固定 prompt を持って disposable Worker / 専用 Pod を立ち上げている経路がいくつかある:
|
||||
|
||||
- Phase 1 活動抽出(`crates/memory/src/extract/prompt.rs::EXTRACT_SYSTEM_PROMPT`)
|
||||
- Phase 2 統合 + 整理(`tickets/memory-phase2-consolidation.md`、本チケット時点では実装中 / 直前)
|
||||
- extract 活動抽出(`crates/memory/src/extract/prompt.rs::EXTRACT_SYSTEM_PROMPT`)
|
||||
- consolidation 統合 + 整理(`tickets/memory-consolidation.md`、本チケット時点では実装中 / 直前)
|
||||
- Compact(`PromptCatalog::compact_system`)
|
||||
|
||||
これらは実装内 `&str` 定数や `PromptCatalog` の overlay で管理されており、prompt の調整や運用カスタマイズが「コード変更 + 再ビルド」を要する。一方、ユーザー向け `/<slug>` Workflow(`tickets/workflow.md`)は `<workspace_root>/.insomnia/workflow/<slug>.md` に住み、frontmatter + Markdown 本文 + `requires` Knowledge inject を持つ宣言形式で運用できる。
|
||||
|
|
@ -13,7 +13,7 @@ INSOMNIA が内部で固定 prompt を持って disposable Worker / 専用 Pod
|
|||
両者を寄せ、内部 Worker / 内部 Pod の prompt + ツール surface + Knowledge 依存を **Workflow と同一仕様で記述** できる経路を用意する。これにより:
|
||||
|
||||
- 内部 prompt の運用調整が workspace 側でできる(コード変更不要)
|
||||
- Phase 2 の prompt 案 (`docs/plan/memory-prompts.md`) を workspace に直接 ingest できる
|
||||
- consolidation の prompt 案 (`docs/plan/memory-prompts.md`) を workspace に直接 ingest できる
|
||||
- 将来 consolidation を独立 Pod に引き上げる際も、Workflow を submit する形に揃えられる
|
||||
|
||||
## 要件
|
||||
|
|
@ -29,7 +29,7 @@ INSOMNIA が内部で固定 prompt を持って disposable Worker / 専用 Pod
|
|||
|
||||
### 内部呼び出し経路
|
||||
|
||||
Pod 側の既存トリガー(Phase 1 post-run / Phase 2 staging 閾値 / Compact 閾値 等)は固定 `&str` の代わりに Workflow loader 経由で:
|
||||
Pod 側の既存トリガー(extract post-run / consolidation staging 閾値 / Compact 閾値 等)は固定 `&str` の代わりに Workflow loader 経由で:
|
||||
|
||||
1. 内部識別キーで該当 Workflow を解決(衝突時は workspace 上書き優先、なければ insomnia bundled default)
|
||||
2. `requires` Knowledge を本文の前に inject
|
||||
|
|
@ -46,8 +46,8 @@ Pod 側の既存トリガー(Phase 1 post-run / Phase 2 staging 閾値 / Compa
|
|||
### 関連チケットとの順序
|
||||
|
||||
- `tickets/workflow.md`(ユーザー向け Workflow 本体)が先行する。本チケットはその仕様を前提に「内部呼び出し経路」を追加する側
|
||||
- `tickets/memory-phase2-consolidation.md` は当面 `&str` 定数で実装してよい。本チケット完了時に Workflow 化に乗り換える
|
||||
- Phase 1 / Compact も同様に role ごとに段階移行
|
||||
- `tickets/memory-consolidation.md` は当面 `&str` 定数で実装してよい。本チケット完了時に Workflow 化に乗り換える
|
||||
- extract / Compact も同様に role ごとに段階移行
|
||||
|
||||
## 範囲外
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ Pod 側の既存トリガー(Phase 1 post-run / Phase 2 staging 閾値 / Compa
|
|||
|
||||
## 完了条件
|
||||
|
||||
- 各内部 Worker / 内部 Pod(少なくとも Phase 1 / Phase 2 / Compact のうち、本チケット着手時点で実装済みのもの)が内部識別キー付き Workflow を解決して prompt とツール surface を組み立てる
|
||||
- 各内部 Worker / 内部 Pod(少なくとも extract / consolidation / Compact のうち、本チケット着手時点で実装済みのもの)が内部識別キー付き Workflow を解決して prompt とツール surface を組み立てる
|
||||
- workspace で `.insomnia/workflow/<slug>.md` を上書きすれば内部 Worker の prompt が変わる
|
||||
- workspace に該当 Workflow が無い場合、bundled default が使われる
|
||||
- `user_invocable: false` の内部 Workflow は `/<slug>` 候補から除外され、ユーザーからは呼べない
|
||||
|
|
@ -68,6 +68,6 @@ Pod 側の既存トリガー(Phase 1 post-run / Phase 2 staging 閾値 / Compa
|
|||
## 参照
|
||||
|
||||
- 前提: `tickets/workflow.md`
|
||||
- 最初の利用者: `tickets/memory-phase2-consolidation.md`
|
||||
- 最初の利用者: `tickets/memory-consolidation.md`
|
||||
- 関連: `tickets/agent-skills.md`(外部 SKILL ingest 経路。本チケットの内部呼び出し経路とは別軸)
|
||||
- 設計: `docs/plan/workflow.md`、`docs/plan/memory.md`、`docs/plan/memory-prompts.md`
|
||||
|
|
|
|||
54
tickets/memory-audit-log.md
Normal file
54
tickets/memory-audit-log.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# メモリ機構: extract / consolidation 監査ログ
|
||||
|
||||
## 背景
|
||||
|
||||
Memory extract と consolidation は、session log / staging / memory / knowledge をまたいで自律的に記録を作成・更新する。動作結果は最終的には `memory/*` / `knowledge/*` と git diff で確認できるが、実行中に何が起きているか、人間が `tail -f` 相当で追える観測面がない。
|
||||
|
||||
特に consolidation は rewrite / merge / trim / drop を許可するため、あとから最終 diff だけを見るよりも、run lifecycle と memory tool 操作の監査ログがある方が挙動を理解しやすい。
|
||||
|
||||
## 方針
|
||||
|
||||
workspace の `.insomnia/memory/_logs/` に append-only な JSONL ログを出す。拡張子は `.log` とし、1 行 1 event で `tail -f` できる形式にする。
|
||||
|
||||
ログは source of truth ではなく監査・観測用である。正本は従来通り session log、staging、`memory/*`、`knowledge/*`、git diff とする。consolidation の入力や memory 採択判断がこのログに依存する設計にはしない。
|
||||
|
||||
## 要件
|
||||
|
||||
- `.insomnia/memory/_logs/` 配下に JSONL `.log` を append する仕組みを追加する。
|
||||
- 具体的なローテーション単位は実装で決めてよいが、`tail -f` しやすい最新ログ導線を用意する。
|
||||
- 例: 日次 `memory-YYYY-MM-DD.log`、または run 単位 log + `current.log`。
|
||||
- extract の lifecycle event を記録する。
|
||||
- started / completed / failed
|
||||
- run id
|
||||
- session id と処理対象 range
|
||||
- staging に書いた件数・path / id の概要
|
||||
- 取得できる場合は model / usage
|
||||
- consolidation の lifecycle event を記録する。
|
||||
- started / completed / failed
|
||||
- run id
|
||||
- consumed staging id list または count
|
||||
- 書き込み概要
|
||||
- 取得できる場合は model / usage
|
||||
- memory / knowledge 専用 write/edit/delete tool の操作を audit event として記録する。
|
||||
- `kind`, `slug`, `path`, `op`, `status`
|
||||
- 可能なら before / after hash
|
||||
- Linter failure も失敗 event として残す
|
||||
- ログは通常の LLM context に暗黙注入しない。
|
||||
- 人間が `tail -f` するための観測面とする。
|
||||
- LLM が読む必要がある場合は通常の tool read 経由にし、history に残る経路を使う。
|
||||
- `_staging` とは分離し、consolidation の処理対象にしない。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- extract run の開始・終了・失敗が `.insomnia/memory/_logs/*.log` に JSONL で追記される。
|
||||
- consolidation run の開始・終了・失敗が同ログに JSONL で追記される。
|
||||
- memory / knowledge 専用 tool による write/edit/delete と Linter failure が同ログに JSONL で追記される。
|
||||
- 最新ログを `tail -f` する運用手順がドキュメントまたはコメントから分かる。
|
||||
- ログが memory / knowledge の正本や consolidation 入力として扱われない。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- ログ viewer UI。
|
||||
- ログを使った自動 rollback。
|
||||
- ログを使った Knowledge 採択 / 整理判定。
|
||||
- session-store の正本イベント形式の変更。
|
||||
54
tickets/memory-phase-naming.md
Normal file
54
tickets/memory-phase-naming.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Memory: extract / consolidation 呼称を extract / consolidation に統一
|
||||
|
||||
## 背景
|
||||
|
||||
メモリサブシステムは公開 API 側では既に `extract` / `consolidate` (consolidation) を識別子として採用している:
|
||||
|
||||
- モジュール: `crates/memory/src/extract/`, `crates/memory/src/consolidate/`
|
||||
- Manifest フィールド: `extract_model`, `extract_threshold`, `extract_worker_max_input_tokens`, `consolidation_model`, `consolidation_threshold_files`, `consolidation_threshold_bytes` (`crates/manifest/src/lib.rs:99-131`)
|
||||
- `pub fn` / `pub struct` で `phase` を含む識別子は 0 件、共通の `Phase` enum / trait も存在しない
|
||||
|
||||
一方、コメント・ログ・エラー文字列・LLM プロンプト・ドキュメントには「extract」「consolidation」という呼称が広く残っている。設計的に "phase" という抽象が不要であるにもかかわらず二重の語彙が並走している状態で、新規読者にとって「extract = extract、consolidation = consolidation」というマッピングを暗黙に要求している。
|
||||
|
||||
特に LLM プロンプト (`resources/prompts/internal/memory_*_system.md`, `crates/memory/src/consolidate/input.rs:57-59`) に "consolidation" が出現しているのは、モデルの行動を機能名ではなく序数で誘導している形になり、置き換えるとついでに改善になる。
|
||||
|
||||
## 方針
|
||||
|
||||
"extract" を "extract"、"consolidation" を "consolidation" に置き換える。コードに `phase` という共通抽象が無いことを名前の上でも明示する。
|
||||
|
||||
「consolidation 内部の `consolidation phase` / `tidy phase`」のように phase という語が **階層的に再利用されている表現**は単純置換すると壊れるので、その箇所だけ語彙ごと整理する(例: `consolidation step` / `tidy step`、または「consolidation の統合パート / 整理パート」など、文脈に応じて)。
|
||||
|
||||
## 要件
|
||||
|
||||
- Rust コード (`crates/`) 内の doc comment / 通常コメント / ログメッセージ / `thiserror` の `#[error]` 文字列 / テスト関数名・コメントから "extract" / "consolidation" を排除し、`extract` / `consolidation` に置き換える。
|
||||
- LLM プロンプト用の固定文字列 (`crates/memory/src/consolidate/input.rs` の `consolidation input. Run the consolidation phase first ...`) を `consolidation` 系の語彙のみで再構成する。`resources/prompts/internal/memory_extract_system.md` と `resources/prompts/internal/memory_consolidation_system.md` の `phase` 言及も同様に置換する。
|
||||
- `docs/plan/memory.md` と関連 plan / ref 文書 (`docs/plan/memory-effectiveness.md`, `docs/plan/memory-prompts.md`, `docs/ref/memory-systems.md`, `docs/ref/opencode-comparison.md` の memory 周辺) の "extract / consolidation" を改稿する。章立てや見出しに使われている場合は「## Extract」「## Consolidation」の構成に置き換える。
|
||||
- `phase` という語を残してよいのは「(memory と無関係な) tool dispatch / TUI / worker 内部の局所的なフェーズ表現」(例: `crates/llm-worker/src/worker.rs:747,795`、`crates/tui/src/spawn.rs:148,182`、`crates/tui/src/main.rs:209`)。これは memory の話ではないので対象外。
|
||||
- 後方互換 shim は不要(公開 API 名は変更しない、テキストの置き換えのみ)。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- `git grep -i 'extract\|consolidation\|phase1\|phase2'` の結果から、memory サブシステムに紐づく hit が 0 になる(TUI / tool dispatch / 一般語 "thinking phase" は除く)。
|
||||
- LLM プロンプト 3 ファイル (`memory_extract_system.md`, `memory_consolidation_system.md`, `consolidate/input.rs` の inline テキスト) で序数表現が消え、機能名のみで指示が成立している。
|
||||
- `docs/plan/memory.md` の見出し構造と本文が `Extract` / `Consolidation` ベースに揃っている。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- 公開 API 名 (`extract_*`, `consolidation_*`) の rename。既に統一されているので変更しない。
|
||||
- メモリの設計・挙動変更。純粋に呼称の整理のみ。
|
||||
- TUI / worker / tool dispatch などメモリ外の "extract / 2" 言及。
|
||||
- ファイル名・モジュール名の変更(既に `extract/` / `consolidate/`)。
|
||||
|
||||
## 影響範囲
|
||||
|
||||
- `crates/manifest/src/lib.rs`, `crates/manifest/src/defaults.rs`: 該当 doc comment。
|
||||
- `crates/memory/src/extract/{mod,input,payload,pointer,staging,tool}.rs`: doc comment。
|
||||
- `crates/memory/src/consolidate/{mod,input,lock,staging,tidy}.rs`: doc comment、ログ、`#[error]`、LLM プロンプト inline 文字列。
|
||||
- `crates/memory/src/workspace.rs:186`: コメント 1 件。
|
||||
- `crates/llm-worker/src/token_counter.rs:11`, `crates/pod/src/compact/token_counter.rs:154`: doc comment。
|
||||
- `crates/pod/src/prompt/catalog.rs:64,66`: doc comment。
|
||||
- `crates/pod/tests/compact_events_test.rs`: テスト関数名 (`compact_resets_extract_pointer_so_phase1_can_fire_again` 他) とコメント。
|
||||
- `crates/pod/tests/consolidation_test.rs`: コメント。
|
||||
- `resources/prompts/internal/memory_extract_system.md`, `resources/prompts/internal/memory_consolidation_system.md`: プロンプト本文。
|
||||
- `docs/plan/memory.md`, `docs/plan/memory-effectiveness.md`, `docs/plan/memory-prompts.md`, `docs/ref/memory-systems.md`, `docs/ref/opencode-comparison.md` の該当箇所。
|
||||
- `docs/manifest.toml`: 該当があれば。
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## 背景
|
||||
|
||||
`docs/plan/memory.md` §使用頻度メトリクス の実装。memory 検索ツール / Knowledge 検索ツール内に invoke 計測フックを入れ、時間単位ではなく累積 input token で正規化した頻度を算出する。Phase 2 統合 phase の Knowledge 新規作成 gate と Phase 2 整理 phase の保護閾値の両方で使われる。
|
||||
`docs/plan/memory.md` §使用頻度メトリクス の実装。memory 検索ツール / Knowledge 検索ツール内に invoke 計測フックを入れ、時間単位ではなく累積 input token で正規化した頻度を算出する。consolidation 統合 step の Knowledge 新規作成 gate と consolidation 整理 step の保護閾値の両方で使われる。
|
||||
|
||||
## 要件
|
||||
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
- 累積方式: 最大 10 回前の invoke から現在までの時系列窓でフィルタして集計
|
||||
- **Knowledge 化候補レポート**:
|
||||
- 対象は `memory/*` 配下の record(Phase 1 成果物の decisions / requests + 既存 knowledge)
|
||||
- 対象は `memory/*` 配下の record(extract 成果物の decisions / requests + 既存 knowledge)
|
||||
- 明示 invoke 頻度が閾値超過のものを列挙
|
||||
- 同一 session 内の連続参照は 1 count に丸める
|
||||
- 複数 session での再参照を要件とする(spike 除外)
|
||||
|
|
@ -36,12 +36,12 @@
|
|||
|
||||
### 消費者
|
||||
|
||||
- Phase 2 Worker の統合 phase 入力として候補レポートを渡す
|
||||
- Phase 2 Worker の整理 phase で保護閾値判定(明示 invoke frequency >= 1.0 invokes/Mtoken)に使う
|
||||
- consolidation Worker の統合 step 入力として候補レポートを渡す
|
||||
- consolidation Worker の整理 step で保護閾値判定(明示 invoke frequency >= 1.0 invokes/Mtoken)に使う
|
||||
|
||||
## 範囲外
|
||||
|
||||
- Phase 2 整理 phase の実装本体(`memory-phase2-consolidation` 側。本チケットは保護閾値判定に必要なメトリクスの提供まで)
|
||||
- consolidation 整理 step の実装本体(`memory-consolidation` 側。本チケットは保護閾値判定に必要なメトリクスの提供まで)
|
||||
- `model_invokation` ON/OFF の自動判定ロジック(将来検討)
|
||||
- Shallow request の自動除外(将来検討)
|
||||
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
|
||||
- 検索ツール呼び出しで invoke event が workspace 側に積まれる
|
||||
- `model_invokation` 注入のコスト側集計が別口で積まれる
|
||||
- 候補レポート API が Phase 2 Worker の起動時に呼べる
|
||||
- 候補レポート API が consolidation Worker の起動時に呼べる
|
||||
- 閾値未満の record は候補レポートに載らない
|
||||
- 同一 session 内連続参照は 1 count に丸まる
|
||||
|
||||
|
|
@ -57,4 +57,4 @@
|
|||
|
||||
- `docs/plan/memory.md` §使用頻度メトリクス / §判断ルール / §retrieval 経路
|
||||
- `tickets/memory-search-tools.md`(hook 挿入点)
|
||||
- `tickets/memory-phase2-consolidation.md`(統合 / 整理 両 phase の消費者)
|
||||
- `tickets/memory-consolidation.md`(統合 / 整理 両 step の消費者)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ mizchi の empirical-prompt-tuning は、agent-facing な指示(Skill / slash
|
|||
- usage tokens
|
||||
- workflow / knowledge の明示 invoke 頻度(`tickets/memory-usage-metrics.md`)
|
||||
- `model_invokation` 常駐注入のコスト側指標
|
||||
- Phase 1 / Phase 2 memory consolidation による recurring pattern 抽出
|
||||
- extract / consolidation による recurring pattern 抽出
|
||||
- Workflow 自動書き込み禁止に基づく improvement offer
|
||||
|
||||
したがって、`/empirical-prompt-tuning` 相当の Workflow は、評価実行を orchestration するだけでなく、評価結果を構造化 event として残し、将来的に memory consolidation / usage metrics / Workflow improvement offer / `model_invokation` 判断へ接続するべきである。
|
||||
|
|
@ -91,8 +91,8 @@ retries
|
|||
|
||||
接続方針:
|
||||
|
||||
- evaluator self-report は Phase 1 の活動抽出対象になる
|
||||
- repeated `General Fix Rule` は Phase 2 が recurring failure pattern として統合できる
|
||||
- evaluator self-report は consolidation extract の活動抽出対象になる
|
||||
- repeated `General Fix Rule` は consolidation が recurring failure pattern として統合できる
|
||||
- recurring pattern は即 Knowledge 化せず、使用頻度メトリクス gate を通す
|
||||
- Workflow 改善は `.insomnia/workflow/*.md` へ自動書き込みせず、Notification / report / ticket などの offer に留める
|
||||
- `model_invokation` ON 判断では、明示 invoke 頻度と常駐コストに加えて、eval success rate / unclear point count / description-body consistency を判断材料にする
|
||||
|
|
@ -116,7 +116,7 @@ Claude Code 版の `tool_uses` を、insomnia では tool 種別ごとの偏り
|
|||
手書き台帳だけにせず、eval event から抽出可能な failure pattern として扱う。
|
||||
|
||||
- `General Fix Rule` を class-level pattern として正規化する
|
||||
- 同じ pattern が複数 scenario / 複数 iteration / 複数 target で再発した場合、Phase 2 が decision / knowledge candidate / workflow improvement offer に統合できる
|
||||
- 同じ pattern が複数 scenario / 複数 iteration / 複数 target で再発した場合、consolidation が decision / knowledge candidate / workflow improvement offer に統合できる
|
||||
- 同じ pattern が 3 回以上再発した場合、局所 patch ではなく target prompt の構造変更を提案する
|
||||
|
||||
## 範囲外
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user