@team-agent/installer 0.5.31 → 0.5.33

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.
@@ -764,9 +764,31 @@ impl Coordinator {
764
764
  // refreshed BEFORE classification (the classifier sees the updated value).
765
765
  // A non-empty but UNCHANGED capture must not dirty the state every tick
766
766
  // (P3 umbrella: steady second tick is a zero state write).
767
+ //
768
+ // 0.5.32 follow-up (`.team/artifacts/0532-r1-real-fail-triage.md` §6):
769
+ // fresh-cohort startup-output guard. After the restart clear helper wipes
770
+ // `activity`/`worker_state`/`last_output_at`/`last_output_hash`, the first
771
+ // non-empty pane capture (fake worker's READY line, provider startup
772
+ // banner) is trivially different from the missing digest. If we let it
773
+ // set `last_output_at=now`, classify_agent_activity falls through to
774
+ // `recent_provider_output => Working` even though no structural signal
775
+ // exists — that's the R1 real-machine false busy.
776
+ //
777
+ // Narrow gate: fresh cohort (no activity/worker_state/last_output_at
778
+ // /last_output_hash/current turn/task fields), no work obligation
779
+ // arming a real send, and no structural prompt/working signal in the
780
+ // capture → still update `last_output_hash` (dedup) but do NOT write
781
+ // `last_output_at`. The classifier then returns Uncertain=UNKNOWN,
782
+ // which is the honest post-clear observation. Preserves recent_provider_output
783
+ // for non-startup normal output (later capture has prior last_output_at
784
+ // or last_output_hash, so `fresh_cohort` false → gate skipped).
785
+ let fresh_cohort = agent_is_fresh_cohort(agent);
786
+ let has_structural_signal = latest_pane_signal_is_structural(&captured.text);
767
787
  let output_advanced =
768
788
  !captured.text.is_empty() && scrollback_digest_advanced(agent, &captured.text);
769
- if output_advanced {
789
+ let suppress_last_output_at =
790
+ output_advanced && fresh_cohort && !has_work_obligation && !has_structural_signal;
791
+ if output_advanced && !suppress_last_output_at {
770
792
  if let Some(agent_obj) = agent.as_object_mut() {
771
793
  agent_obj.insert(
772
794
  "last_output_at".to_string(),
@@ -1319,7 +1341,7 @@ impl Coordinator {
1319
1341
  }
1320
1342
 
1321
1343
  /// `message_store_schema_health`(`lifecycle.py:197`)。DB 列兼容门:区分 pre-init 必需列缺失
1322
- /// (拒启)vs migratable 列缺失(可迁移)。`advanced repair-state --schema` 用其 action hint。
1344
+ /// (拒启)vs migratable 列缺失(可迁移)。`doctor --fix-schema` 用其 action hint。
1323
1345
  pub fn schema_health(&self) -> SchemaHealth {
1324
1346
  // A-8: the gate must inspect the REAL team.db (Python lifecycle.py:197+
1325
1347
  // message_store_schema_health); a hardcoded ok:true left the card §89
@@ -1807,6 +1829,41 @@ fn parse_rfc3339_utc(raw: &str) -> Option<chrono::DateTime<chrono::Utc>> {
1807
1829
  .map(|value| value.with_timezone(&chrono::Utc))
1808
1830
  }
1809
1831
 
1832
+ /// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
1833
+ /// return true when the rollout file's modification time is strictly older
1834
+ /// than the agent's current `spawned_at`. Used by `jsonl_activity_for_agent`
1835
+ /// to refuse classifying a pre-spawn transcript tail into fresh-cohort
1836
+ /// activity. Missing `spawned_at`, missing mtime, or unparseable timestamp
1837
+ /// → false (do not block classification; the caller keeps the pre-0.5.32
1838
+ /// behavior).
1839
+ ///
1840
+ /// 0.5.33 boundary tightening (leader 派单 msg_42dbad7f2938 / earlier cr
1841
+ /// batch older-or-equal 预警): use strict `<` not `<=`. Fast machines can
1842
+ /// produce a fresh post-spawn rollout whose mtime shares the second-resolution
1843
+ /// `spawned_at`; treating "equal" as pre-spawn would wrongly reject the
1844
+ /// first post-spawn transcript record. Genuinely pre-spawn transcripts have
1845
+ /// mtime strictly before `spawned_at` and remain rejected. The equal case
1846
+ /// falls back to the classifier + pane fallback, which is the ambiguous-
1847
+ /// but-safe path.
1848
+ fn jsonl_older_than_spawn_boundary(agent: &Value, metadata: &std::fs::Metadata) -> bool {
1849
+ let Some(spawned_at) = agent
1850
+ .get("spawned_at")
1851
+ .and_then(Value::as_str)
1852
+ .and_then(parse_rfc3339_utc)
1853
+ else {
1854
+ return false;
1855
+ };
1856
+ let Ok(mtime) = metadata.modified() else {
1857
+ return false;
1858
+ };
1859
+ let Ok(mtime_since_epoch) = mtime.duration_since(std::time::UNIX_EPOCH) else {
1860
+ return false;
1861
+ };
1862
+ let mtime_utc =
1863
+ chrono::DateTime::<chrono::Utc>::from(std::time::UNIX_EPOCH + mtime_since_epoch);
1864
+ mtime_utc < spawned_at
1865
+ }
1866
+
1810
1867
  fn matching_capture_pane_info(
1811
1868
  agent: &Value,
1812
1869
  session: &crate::transport::SessionName,
@@ -1902,6 +1959,16 @@ fn jsonl_activity_for_agent(agent: &Value) -> Option<crate::messaging::AgentActi
1902
1959
  let metadata = std::fs::metadata(&rollout_path).ok()?;
1903
1960
  let size = metadata.len();
1904
1961
  let mtime_ns = crate::coordinator::steps::abnormal::metadata_mtime_ns(&metadata)?;
1962
+ // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
1963
+ // freshness gate — a rollout file whose last modification is older than
1964
+ // the agent's current `spawned_at` cannot describe post-spawn behavior.
1965
+ // Refusing to classify (return None) here keeps stale pre-restart
1966
+ // Working tails from re-poisoning a fresh cohort's activity/worker_state;
1967
+ // pane fallback in the caller is still free to produce a fresh
1968
+ // classification when a live pane observation exists.
1969
+ if jsonl_older_than_spawn_boundary(agent, &metadata) {
1970
+ return None;
1971
+ }
1905
1972
  if let Ok(cache) = jsonl_activity_cache().lock() {
1906
1973
  if let Some(entry) = cache.get(&rollout_path) {
1907
1974
  if entry.size == size && entry.mtime_ns == mtime_ns {
@@ -2211,6 +2278,48 @@ fn clear_awaiting_human_confirm(agent: &mut Value) {
2211
2278
  }
2212
2279
  }
2213
2280
 
2281
+ /// 0.5.32 follow-up (`.team/artifacts/0532-r1-real-fail-triage.md` §6/§7):
2282
+ /// "fresh cohort" = the state a restart / start-agent clear helper leaves
2283
+ /// behind, before any post-spawn tick has repopulated observations.
2284
+ /// Requires `spawned_at` present so the gate fires only for real
2285
+ /// restart/start cohorts; coordinator-only fixtures without a spawn
2286
+ /// boundary keep pre-0.5.32 pane fallback behavior (spine2 sync_health
2287
+ /// tests seed bare provider/window/pane_id and expect `last_output_at`
2288
+ /// to be recorded on the first delta).
2289
+ fn agent_is_fresh_cohort(agent: &Value) -> bool {
2290
+ let spawned_at_present = agent
2291
+ .get("spawned_at")
2292
+ .and_then(Value::as_str)
2293
+ .is_some_and(|s| !s.is_empty());
2294
+ if !spawned_at_present {
2295
+ return false;
2296
+ }
2297
+ const CLEARED_FIELDS: &[&str] = &[
2298
+ "activity",
2299
+ "worker_state",
2300
+ "last_output_at",
2301
+ "last_output_hash",
2302
+ "current_turn_message_id",
2303
+ "current_task_id",
2304
+ "task_id",
2305
+ ];
2306
+ CLEARED_FIELDS.iter().all(|field| {
2307
+ agent
2308
+ .get(*field)
2309
+ .map(|value| value.is_null())
2310
+ .unwrap_or(true)
2311
+ })
2312
+ }
2313
+
2314
+ /// 0.5.32 follow-up: is there a structural pane signal in `text`?
2315
+ /// True when `latest_prompt_signal` recognizes anything (idle prompt char,
2316
+ /// working spinner/keyword, or fake READY/WORKING marker). When true, the
2317
+ /// classifier will produce a definite non-`recent_provider_output` activity
2318
+ /// on its own, so the freshness gate must not suppress `last_output_at`.
2319
+ fn latest_pane_signal_is_structural(text: &str) -> bool {
2320
+ crate::messaging::helpers::latest_prompt_signal(text).is_some()
2321
+ }
2322
+
2214
2323
  /// Python approvals/status.py:68-72 — sha256 the scrollback, compare to the stored
2215
2324
  /// `last_output_hash`; only a CHANGED digest counts as advanced output (and stores
2216
2325
  /// the new digest).
@@ -94,3 +94,27 @@ fn delete_agent_health(workspace: &Path, agent_id: &AgentId) -> Result<(), crate
94
94
  )?;
95
95
  Ok(())
96
96
  }
97
+
98
+ /// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
99
+ /// clear the `(owner_team_id, agent_id)` health observation row on a new
100
+ /// worker process cohort boundary. Distinct from `delete_agent_health` (used
101
+ /// by remove-agent rollback): this narrow helper keyed on both owner_team_id
102
+ /// and agent_id so a sibling team with the same agent_id keeps its own row.
103
+ /// Silently no-ops when the row is absent.
104
+ pub fn clear_agent_health_observation(
105
+ workspace: &Path,
106
+ owner_team_id: &str,
107
+ agent_id: &AgentId,
108
+ ) -> Result<(), crate::db::DbError> {
109
+ if owner_team_id.is_empty() {
110
+ return Ok(());
111
+ }
112
+ let store = crate::message_store::MessageStore::open(workspace)
113
+ .map_err(|e| crate::db::DbError::Schema(e.to_string()))?;
114
+ let conn = crate::db::schema::open_db(store.db_path())?;
115
+ conn.execute(
116
+ "delete from agent_health where owner_team_id = ?1 and agent_id = ?2",
117
+ rusqlite::params![owner_team_id, agent_id.as_str()],
118
+ )?;
119
+ Ok(())
120
+ }
@@ -284,6 +284,14 @@ pub(crate) fn start_agent_at_paths(
284
284
  // after spawn" race with coordinator and no double source of truth.
285
285
  crate::lifecycle::launch::annotate_runtime_tmux_endpoint(&mut state, transport, workspace);
286
286
  let team_key = restart_projection_team_key(&state, team);
287
+ // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
288
+ // clear the matching `agent_health` observation on the new spawn cohort
289
+ // so five-line status summary and status --json do not surface a stale
290
+ // WORKING row from the pre-shutdown process. Best-effort: DB failure
291
+ // must not fail the start-agent path.
292
+ let _ = crate::db::agent_health_capture::clear_agent_health_observation(
293
+ workspace, &team_key, agent_id,
294
+ );
287
295
  let skip_capture_backfill = if matches!(
288
296
  start_mode,
289
297
  StartMode::Fresh | StartMode::FreshAfterMissingRollout
@@ -977,6 +985,12 @@ fn mark_agent_started(
977
985
  agent_id
978
986
  )));
979
987
  };
988
+ // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
989
+ // a successful new process cohort invalidates the per-agent
990
+ // turn/activity observation set. Do this before overwriting the
991
+ // lifecycle/topology fields so absence == UNKNOWN until the next
992
+ // coordinator tick or pane fallback produces a post-spawn observation.
993
+ clear_agent_runtime_activity_observation(agent);
980
994
  // S1-CAPTURE-001 (0.4.8, CR M3 provider-agnostic): on a Fresh /
981
995
  // FreshAfterMissingRollout start, the prior session's authoritative
982
996
  // capture tuple MUST be cleared before persist_command_plan_state
@@ -1601,6 +1601,33 @@ pub(super) fn state_spawn_epoch_for_agent(workspace: &Path, agent_id: &AgentId)
1601
1601
  .unwrap_or(0)
1602
1602
  }
1603
1603
 
1604
+ /// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
1605
+ /// clear the per-agent turn/activity observation set on a successful new
1606
+ /// worker process cohort. Called from `mark_agent_started` /
1607
+ /// `mark_agent_respawned` / `mark_fake_harness_agent_respawned`; NOT from
1608
+ /// `mark_agent_running_noop` (no new process was created there).
1609
+ ///
1610
+ /// Removes only observation fields — never lifecycle/topology fields. After
1611
+ /// clearing, absence is UNKNOWN, not synthesized idle (T7 unknown-never-idle
1612
+ /// discipline); the next post-spawn tick may repopulate from JSONL freshness
1613
+ /// gate + pane fallback.
1614
+ pub(super) fn clear_agent_runtime_activity_observation(
1615
+ agent: &mut serde_json::Map<String, serde_json::Value>,
1616
+ ) {
1617
+ for field in [
1618
+ "activity",
1619
+ "worker_state",
1620
+ "last_output_at",
1621
+ "last_output_hash",
1622
+ "current_turn_message_id",
1623
+ "current_task_id", // ALLOWED-LEGACY-READ (Phase-DX E2): cleanup removal of legacy display-only key on new spawn cohort; helper does not read the value.
1624
+ "task_id",
1625
+ "coordinator_idle_capture_next_at",
1626
+ ] {
1627
+ agent.remove(field);
1628
+ }
1629
+ }
1630
+
1604
1631
  #[cfg(test)]
1605
1632
  mod e36_transcript_backing_tests {
1606
1633
  use super::*;
@@ -433,8 +433,7 @@ fn restart_with_selected_team_and_transport(
433
433
  &decision.agent_id,
434
434
  &raw_agent,
435
435
  );
436
- if endpoint_convergence_fake_harness_enabled(&state)
437
- && is_fake_model_harness_agent(&agent)
436
+ if endpoint_convergence_fake_harness_enabled(&state) && is_fake_model_harness_agent(&agent)
438
437
  {
439
438
  write_fake_harness_spawn_argv_event(
440
439
  &selected.run_workspace,
@@ -449,6 +448,13 @@ fn restart_with_selected_team_and_transport(
449
448
  &session_name,
450
449
  &selected.team_key,
451
450
  );
451
+ // 0.5.32: fake harness respawn shares the same spawn cohort
452
+ // boundary — clear the matching `agent_health` observation.
453
+ let _ = crate::db::agent_health_capture::clear_agent_health_observation(
454
+ &selected.run_workspace,
455
+ &selected.team_key,
456
+ &decision.agent_id,
457
+ );
452
458
  successful_agents.push(decision.clone());
453
459
  continue;
454
460
  }
@@ -556,6 +562,15 @@ fn restart_with_selected_team_and_transport(
556
562
  {
557
563
  persist_effective_approval_policy_for_restart(agent, &safety);
558
564
  }
565
+ // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
566
+ // pair the state-side activity clear with a DB `agent_health` clear so
567
+ // status --json's health projection does not surface the pre-restart
568
+ // WORKING row. Best-effort: DB failure must not fail the restart loop.
569
+ let _ = crate::db::agent_health_capture::clear_agent_health_observation(
570
+ &selected.run_workspace,
571
+ &selected.team_key,
572
+ &decision.agent_id,
573
+ );
559
574
  }
560
575
  // END_B5_RESTART_ISOLATION_LOOP
561
576
  let mut topology_authority_agent_ids = successful_agents
@@ -1630,6 +1645,12 @@ fn mark_agent_respawned(
1630
1645
  agent_id
1631
1646
  )));
1632
1647
  };
1648
+ // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
1649
+ // multi-worker restart respawn is a new process cohort; clear the
1650
+ // per-agent turn/activity observation set before overwriting lifecycle
1651
+ // fields so stale `activity=working` / `worker_state=BUSY` do not
1652
+ // survive into the fresh cohort.
1653
+ clear_agent_runtime_activity_observation(agent);
1633
1654
  agent.insert("status".to_string(), serde_json::json!("running"));
1634
1655
  agent.insert(
1635
1656
  "window".to_string(),
@@ -1785,6 +1806,10 @@ fn mark_fake_harness_agent_respawned(
1785
1806
  else {
1786
1807
  return;
1787
1808
  };
1809
+ // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
1810
+ // fake harness respawn is also a new process cohort — RED fixtures must
1811
+ // not preserve stale activity observations across restart.
1812
+ clear_agent_runtime_activity_observation(agent);
1788
1813
  agent.insert("status".to_string(), serde_json::json!("running"));
1789
1814
  agent.insert("window".to_string(), serde_json::json!(agent_id.as_str()));
1790
1815
  agent.insert(
@@ -168,6 +168,19 @@ pub(crate) fn latest_prompt_signal(scrollback: &str) -> Option<AgentActivity> {
168
168
  if line.contains('❯') || line.contains('›') {
169
169
  has_idle_prompt = true;
170
170
  }
171
+ // 0.5.32 follow-up (`0532-r1-real-fail-triage.md` §6/§7):
172
+ // fake provider owns explicit READY/WORKING markers (fake_worker.rs:47/59).
173
+ // Recognize them here as STRUCTURAL bottom-region signals so restart's
174
+ // first-capture READY does not fall through to `recent_provider_output`
175
+ // false busy. Scope: literal `TEAM_AGENT_FAKE_READY`/`TEAM_AGENT_FAKE_WORKING`
176
+ // tokens only — do NOT infer that arbitrary "ready" prose from real
177
+ // providers means idle.
178
+ if line.contains("TEAM_AGENT_FAKE_READY") {
179
+ has_idle_prompt = true;
180
+ }
181
+ if line.contains("TEAM_AGENT_FAKE_WORKING") {
182
+ has_live_working_indicator = true;
183
+ }
171
184
  // codex live spinner shapes (provider/adapter.rs:875-876 markers):
172
185
  // braille spinner, `• Working (`, `Thinking`; claude `✶`/`✢`/`✻`
173
186
  // and Claude Code tool-progress verbs. We look for STRUCTURAL
@@ -57,7 +57,7 @@ fn stuck_cancel_snapshot_delivered_message_ids_uses_golden_status_set() {
57
57
  // is `store.add_result(envelope)`'d (results.py:73) regardless of any in-flight delivery, then the
58
58
  // collection loop collects it when its task_id is a known task (state.tasks) OR message-scoped (msg_+
59
59
  // matching message). NO live in-flight task is required at ingest time. rt-host-b @ c262bf7 saw a
60
- // VALID envelope (validate-result accepts the same) collect to exit-1 / empty output / NOT in the
60
+ // VALID envelope collect to exit-1 / empty output / NOT in the
61
61
  // results table — i.e. the --result-file ingest path was a no-op (results.rs once did
62
62
  // `let _ = (result_file, …)`). This pins the happy path: a valid envelope for a KNOWN task must be
63
63
  // ingested into the results table AND collected, with ok=true. (Completes the previously-deferred
@@ -75,7 +75,7 @@ use crate::model::enums::Provider;
75
75
  // ── REUSE — step 4 event_log(install/upgrade/repair 审计事件;原子替换的 rebuild/rollback 标记)──
76
76
  use crate::event_log::EventLog;
77
77
 
78
- // ── REUSE — step 5 state(uninstall「有 team 在跑勿删」判定经 state 投影;repair-state 转调)──
78
+ // ── REUSE — step 5 state(uninstall「有 team 在跑勿删」判定经 state 投影)──
79
79
  // `state` 操作 state.json = serde_json::Value;此处仅在 fn 签名内经全限定路径引用,避免未用顶层 import。
80
80
 
81
81
  #[cfg(test)]
@@ -400,7 +400,7 @@ pub enum PackagingError {
400
400
  /// step 3 schema/migration 转调错误。
401
401
  #[error("schema: {0}")]
402
402
  Db(#[from] DbError),
403
- /// step 5 state(repair-state / team-running 判定)转调错误。
403
+ /// step 5 state(team-running 判定)转调错误。
404
404
  #[error("state: {0}")]
405
405
  State(String),
406
406
  /// model 校验/解析转调(版本解析等)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.31",
3
+ "version": "0.5.33",
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.5.31",
24
- "@team-agent/cli-darwin-x64": "0.5.31",
25
- "@team-agent/cli-linux-x64": "0.5.31"
23
+ "@team-agent/cli-darwin-arm64": "0.5.33",
24
+ "@team-agent/cli-darwin-x64": "0.5.33",
25
+ "@team-agent/cli-linux-x64": "0.5.33"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -44,7 +44,7 @@ Fresh/reset red line:
44
44
  | Leader pane exists but messages say `leader_not_attached`, `rebind_required`, `team_owner_mismatch`, or leader 被抢 | Leader ownership or receiver binding drift | §2 L3 Claim Or Rebind Leader |
45
45
  | A live team must be recovered without losing worker context | Resume/restart path needed | §3 L2 Restart With Resume First |
46
46
  | Shutdown/restart cleanup status is ambiguous | Need residue-based truth, not a single `ok` field | §4 L1/L2 Cleanup Verification |
47
- | MCP tool call fails with `Transport closed` while the worker still has the payload | Provider-owned MCP stdio child died | §5 Superseded/Pending E23 Fallback Boundary |
47
+ | MCP tool call fails with `Transport closed` while the worker still has the payload | Provider-owned MCP stdio child died | §5 MCP Transport-Dead Payload Handoff |
48
48
 
49
49
  ## 1. L1 Read-Only Triage
50
50
 
@@ -239,30 +239,15 @@ Python 0.2.11 compatibility note:
239
239
 
240
240
  - Python cleanup notes may mention direct tmux commands. They are not listed here as RS recovery prescriptions.
241
241
 
242
- ## 5. Superseded/Pending E23 Fallback Boundary
242
+ ## 5. MCP Transport-Dead Payload Handoff
243
243
 
244
- [contextual: preserved] [level: L1] E23 fallback is the only narrow worker-scope exception to the normal tool boundary. It is for a current payload already in hand after a leader-bound MCP delivery failure.
244
+ [contextual: preserved] [level: L1] If the worker already has a leader-bound payload in hand and the MCP transport dies before the normal `send` or `report_result` path can accept it, preserve the payload in a file handoff artifact and ask for leader/user recovery through the visible channel that still works.
245
245
 
246
- Current status:
247
-
248
- - Product implementation exists in the E23 branch for `fallback-send-leader` and `fallback-report-result`.
249
- - Local RS verification has covered CLI help and focused E23 tests.
250
- - This reference does not yet promote the fallback CLI to a general external prescription because the new documentation gate requires current-release RS validation in the Mac mini environment.
251
-
252
- Until that validation is added:
253
-
254
- - Workers should write a file handoff artifact for the current payload and ask for leader/user recovery.
255
- - Leaders should prefer restart/resume of the affected worker after the current payload is preserved.
256
-
257
- Do not use E23 fallback as a general message path. It does not permit arbitrary worker execution of L2/L3 recovery operations.
258
-
259
- RS verification record:
260
-
261
- - Local only at the time of this reference update: E23 focused tests and CLI help on the merged E23 documentation branch. Mac mini current-release validation is still required before adding exact fallback commands here.
246
+ After restart/resume, use the normal MCP tool path again. Do not treat file handoff as a general message path or as permission to run arbitrary L2/L3 recovery operations.
262
247
 
263
248
  Python 0.2.11 compatibility note:
264
249
 
265
- - Python MCP stdio EOF had no durable reconnect. File handoff was the practical fallback. RS E23 adds product support, but this entry remains gated until real-machine validation is complete.
250
+ - Python MCP stdio EOF had no durable reconnect. File handoff was the practical fallback.
266
251
 
267
252
  ## Held Out Of This Reference
268
253