@team-agent/installer 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -566,7 +566,7 @@ dependencies = [
566
566
 
567
567
  [[package]]
568
568
  name = "team-agent"
569
- version = "0.4.0"
569
+ version = "0.4.1"
570
570
  dependencies = [
571
571
  "anyhow",
572
572
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent"]
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.4.0"
12
+ version = "0.4.1"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -2629,6 +2629,33 @@ fn agent_rollout_path(agent: &Value) -> Option<PathBuf> {
2629
2629
  .map(PathBuf::from)
2630
2630
  }
2631
2631
 
2632
+ /// Memory-growth fix (architect §5): bounded tail cap for JSONL activity reads.
2633
+ /// Matches `ABNORMAL_TAIL_BYTES` (131_072 bytes) — the abnormal-exit path
2634
+ /// already proved this size is sufficient to capture the latest lifecycle
2635
+ /// records across all providers (claude / codex / copilot).
2636
+ const JSONL_ACTIVITY_TAIL_BYTES: u64 = 131_072;
2637
+
2638
+ /// Memory-growth fix (architect §5): per-process `(path, size, mtime_ns) →
2639
+ /// activity` cache. When a rollout file hasn't changed since the previous
2640
+ /// tick, we skip the file read AND the classification entirely. This is the
2641
+ /// dominant savings: a 538MB Claude transcript that updates every few seconds
2642
+ /// is touched only when its size or mtime actually moves. Stored values are
2643
+ /// small (Option<AgentActivity> = enum + short rationale string); we never
2644
+ /// cache the transcript text or parsed JSON.
2645
+ struct JsonlActivityCacheEntry {
2646
+ size: u64,
2647
+ mtime_ns: u64,
2648
+ activity: Option<crate::messaging::AgentActivity>,
2649
+ }
2650
+
2651
+ fn jsonl_activity_cache(
2652
+ ) -> &'static std::sync::Mutex<std::collections::HashMap<PathBuf, JsonlActivityCacheEntry>> {
2653
+ static CACHE: std::sync::OnceLock<
2654
+ std::sync::Mutex<std::collections::HashMap<PathBuf, JsonlActivityCacheEntry>>,
2655
+ > = std::sync::OnceLock::new();
2656
+ CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
2657
+ }
2658
+
2632
2659
  /// E47 (0.3.24 P0, idle/busy 假阳): consult the authoritative provider JSONL
2633
2660
  /// classifier and map to neutral `AgentActivity`. Returns `None` when the
2634
2661
  /// classifier reports `TurnState::Unknown` (unreadable JSONL / no lifecycle
@@ -2638,29 +2665,72 @@ fn agent_rollout_path(agent: &Value) -> Option<PathBuf> {
2638
2665
  /// so we hand off to the TUI scanner which has its OWN no-signal → Uncertain
2639
2666
  /// path. Copilot/Gemini/Fake providers (which don't have JSONL — classify.rs
2640
2667
  /// returns Unknown for them) thus keep using TUI scanning unchanged.
2668
+ ///
2669
+ /// Memory-growth fix (architect analysis 2026-06-23): bounded tail read +
2670
+ /// metadata cache. Pre-fix `std::fs::read_to_string` on a 538MB Claude
2671
+ /// transcript every 5s caused 200MB+ coordinator RSS plateaus from allocator
2672
+ /// fragmentation. Now bounded to 128KiB tail and skipped entirely when
2673
+ /// (size, mtime_ns) is unchanged.
2641
2674
  fn jsonl_activity_for_agent(agent: &Value) -> Option<crate::messaging::AgentActivity> {
2642
2675
  let rollout_path = agent_rollout_path(agent)?;
2643
2676
  let provider = agent
2644
2677
  .get("provider")
2645
2678
  .and_then(Value::as_str)
2646
2679
  .and_then(parse_provider)?;
2647
- let log_text = std::fs::read_to_string(&rollout_path).ok()?;
2680
+
2681
+ // Metadata check + cache lookup. Cache hit when the rollout file has
2682
+ // not changed since the previous tick: return the cached classification
2683
+ // without re-reading the file. Truncation (size shrink with stable mtime)
2684
+ // still forces re-read because size is part of the cache key.
2685
+ let metadata = std::fs::metadata(&rollout_path).ok()?;
2686
+ let size = metadata.len();
2687
+ let mtime_ns = metadata_mtime_ns(&metadata)?;
2688
+ if let Ok(cache) = jsonl_activity_cache().lock() {
2689
+ if let Some(entry) = cache.get(&rollout_path) {
2690
+ if entry.size == size && entry.mtime_ns == mtime_ns {
2691
+ return entry.activity.clone();
2692
+ }
2693
+ }
2694
+ }
2695
+
2696
+ // Cache miss: bounded tail read + classify. The classifier only needs the
2697
+ // latest lifecycle records to determine idle/working state; the
2698
+ // abnormal-exit path uses the same 128KiB tail and is sufficient for
2699
+ // claude / codex / copilot lifecycle markers.
2700
+ let log_text = read_tail_text(&rollout_path, JSONL_ACTIVITY_TAIL_BYTES).ok()?;
2648
2701
  let process = explicit_process_liveness(agent).unwrap_or(ProcessLiveness::Unverifiable);
2649
- let result = crate::provider::classify(provider, &log_text, process, 0.0).ok()?;
2650
- use crate::messaging::{ActivityStatus, AgentActivity};
2651
- use crate::provider::types::TurnState;
2652
- let status = match result.state {
2653
- TurnState::Idle => ActivityStatus::Idle,
2654
- TurnState::IdleInterrupted => ActivityStatus::Idle,
2655
- TurnState::Working => ActivityStatus::Working,
2656
- TurnState::BlockedOnHuman | TurnState::Abnormal => ActivityStatus::Uncertain,
2657
- TurnState::Unknown => return None,
2658
- };
2659
- Some(AgentActivity {
2660
- status,
2661
- confidence: 0.95,
2662
- rationale: format!("provider_jsonl:{}", result.reason),
2663
- })
2702
+ let activity = crate::provider::classify(provider, &log_text, process, 0.0)
2703
+ .ok()
2704
+ .and_then(|result| {
2705
+ use crate::messaging::{ActivityStatus, AgentActivity};
2706
+ use crate::provider::types::TurnState;
2707
+ let status = match result.state {
2708
+ TurnState::Idle => ActivityStatus::Idle,
2709
+ TurnState::IdleInterrupted => ActivityStatus::Idle,
2710
+ TurnState::Working => ActivityStatus::Working,
2711
+ TurnState::BlockedOnHuman | TurnState::Abnormal => ActivityStatus::Uncertain,
2712
+ TurnState::Unknown => return None,
2713
+ };
2714
+ Some(AgentActivity {
2715
+ status,
2716
+ confidence: 0.95,
2717
+ rationale: format!("provider_jsonl:{}", result.reason),
2718
+ })
2719
+ });
2720
+
2721
+ // Store the classification (including None / Unknown) so the next tick
2722
+ // can short-circuit when the file is unchanged.
2723
+ if let Ok(mut cache) = jsonl_activity_cache().lock() {
2724
+ cache.insert(
2725
+ rollout_path,
2726
+ JsonlActivityCacheEntry {
2727
+ size,
2728
+ mtime_ns,
2729
+ activity: activity.clone(),
2730
+ },
2731
+ );
2732
+ }
2733
+ activity
2664
2734
  }
2665
2735
 
2666
2736
  fn runtime_approval_target(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.4.0",
24
- "@team-agent/cli-darwin-x64": "0.4.0",
25
- "@team-agent/cli-linux-x64": "0.4.0"
23
+ "@team-agent/cli-darwin-arm64": "0.4.1",
24
+ "@team-agent/cli-darwin-x64": "0.4.1",
25
+ "@team-agent/cli-linux-x64": "0.4.1"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",