@team-agent/installer 0.3.23 → 0.3.24
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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/leader/lease.rs +34 -13
- package/crates/team-agent/src/leader/tests/lease_api.rs +79 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +11 -3
- package/crates/team-agent/src/lifecycle/launch.rs +2 -16
- package/crates/team-agent/src/messaging/delivery.rs +74 -27
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/wave2.rs +689 -0
- package/crates/team-agent/src/state/projection.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +46 -0
- package/crates/team-agent/src/tmux_backend.rs +17 -5
- package/crates/team-agent/src/transport/test_support.rs +79 -6
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//! + 双写 / 分叉检测。
|
|
3
3
|
|
|
4
4
|
use std::collections::BTreeSet;
|
|
5
|
-
use std::path::Path;
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
6
|
|
|
7
7
|
use serde_json::{json, Value};
|
|
8
8
|
|
|
@@ -1024,14 +1024,27 @@ fn state_owner(state: &Value) -> Option<TeamOwner> {
|
|
|
1024
1024
|
pub fn write_lease_dual_state(workspace: &Path, state: &Value) -> Result<(), LeaderError> {
|
|
1025
1025
|
crate::state::persist::save_runtime_state(workspace, state)?;
|
|
1026
1026
|
if let Some(session_name) = state.get("session_name").and_then(Value::as_str) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1027
|
+
write_team_snapshot_atomic(workspace, session_name, state)?;
|
|
1028
|
+
}
|
|
1029
|
+
Ok(())
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
fn write_team_snapshot_atomic(
|
|
1033
|
+
workspace: &Path,
|
|
1034
|
+
session_name: &str,
|
|
1035
|
+
state: &Value,
|
|
1036
|
+
) -> Result<(), LeaderError> {
|
|
1037
|
+
let snap_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name);
|
|
1038
|
+
let parent = snap_path
|
|
1039
|
+
.parent()
|
|
1040
|
+
.ok_or_else(|| LeaderError::Validation("team snapshot path has no parent".to_string()))?;
|
|
1041
|
+
std::fs::create_dir_all(parent)?;
|
|
1042
|
+
let tmp = parent.join(format!("state.json.tmp-{}", std::process::id()));
|
|
1043
|
+
let data = serde_json::to_vec_pretty(state)?;
|
|
1044
|
+
std::fs::write(&tmp, data)?;
|
|
1045
|
+
if let Err(error) = std::fs::rename(&tmp, &snap_path) {
|
|
1046
|
+
let _ = std::fs::remove_file(&tmp);
|
|
1047
|
+
return Err(error.into());
|
|
1035
1048
|
}
|
|
1036
1049
|
Ok(())
|
|
1037
1050
|
}
|
|
@@ -1113,10 +1126,7 @@ pub fn detect_dual_state_divergence(
|
|
|
1113
1126
|
let Some(session_name) = state.get("session_name").and_then(Value::as_str) else {
|
|
1114
1127
|
return Ok(None);
|
|
1115
1128
|
};
|
|
1116
|
-
let snap_path =
|
|
1117
|
-
.join("teams")
|
|
1118
|
-
.join(session_name)
|
|
1119
|
-
.join("state.json");
|
|
1129
|
+
let snap_path = readable_team_snapshot_path(workspace, session_name);
|
|
1120
1130
|
if !snap_path.exists() {
|
|
1121
1131
|
return Ok(None);
|
|
1122
1132
|
}
|
|
@@ -1150,6 +1160,17 @@ pub fn detect_dual_state_divergence(
|
|
|
1150
1160
|
})))
|
|
1151
1161
|
}
|
|
1152
1162
|
|
|
1163
|
+
fn readable_team_snapshot_path(workspace: &Path, session_name: &str) -> PathBuf {
|
|
1164
|
+
let safe_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name);
|
|
1165
|
+
if safe_path.exists() {
|
|
1166
|
+
return safe_path;
|
|
1167
|
+
}
|
|
1168
|
+
crate::model::paths::runtime_dir(workspace)
|
|
1169
|
+
.join("teams")
|
|
1170
|
+
.join(session_name)
|
|
1171
|
+
.join("state.json")
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1153
1174
|
#[cfg(test)]
|
|
1154
1175
|
mod tests {
|
|
1155
1176
|
use super::*;
|
|
@@ -145,6 +145,85 @@ use super::*;
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
#[test]
|
|
149
|
+
fn lease_divergence_reads_restart_snapshot_for_special_session_names() {
|
|
150
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_dual_special_{}", std::process::id()));
|
|
151
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
152
|
+
let state = serde_json::json!({
|
|
153
|
+
"session_name": "team:proj-中文",
|
|
154
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
|
|
155
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
156
|
+
});
|
|
157
|
+
write_lease_dual_state(&ws, &state).unwrap();
|
|
158
|
+
let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:proj-中文");
|
|
159
|
+
let raw_path = crate::model::paths::runtime_dir(&ws)
|
|
160
|
+
.join("teams")
|
|
161
|
+
.join("team:proj-中文")
|
|
162
|
+
.join("state.json");
|
|
163
|
+
assert!(
|
|
164
|
+
safe_path.exists(),
|
|
165
|
+
"lease snapshot must use the restart-safe path: {}",
|
|
166
|
+
safe_path.display()
|
|
167
|
+
);
|
|
168
|
+
assert!(
|
|
169
|
+
!raw_path.exists(),
|
|
170
|
+
"new lease writes must not create the legacy raw session path: {}",
|
|
171
|
+
raw_path.display()
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
let restart_state = serde_json::json!({
|
|
175
|
+
"session_name": "team:proj-中文",
|
|
176
|
+
"team_owner": {"pane_id": "%9", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
|
|
177
|
+
"leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
|
|
178
|
+
});
|
|
179
|
+
let restart_path = crate::lifecycle::save_team_runtime_snapshot(&ws, &restart_state).unwrap();
|
|
180
|
+
assert_eq!(restart_path, safe_path);
|
|
181
|
+
assert!(
|
|
182
|
+
restart_path.exists(),
|
|
183
|
+
"restart snapshot must be written to a real file: {}",
|
|
184
|
+
restart_path.display()
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
let d = detect_dual_state_divergence(&ws, &state)
|
|
188
|
+
.unwrap()
|
|
189
|
+
.expect("divergence detector must read the restart-written snapshot");
|
|
190
|
+
assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
|
|
191
|
+
assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
|
|
192
|
+
assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
|
|
193
|
+
assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[test]
|
|
197
|
+
fn detect_dual_state_divergence_reads_legacy_raw_snapshot_when_safe_absent() {
|
|
198
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_dual_legacy_{}", std::process::id()));
|
|
199
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
200
|
+
let state = serde_json::json!({
|
|
201
|
+
"session_name": "team:legacy-中文",
|
|
202
|
+
"team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
203
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
204
|
+
});
|
|
205
|
+
let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:legacy-中文");
|
|
206
|
+
assert!(!safe_path.exists(), "test setup requires safe path absent");
|
|
207
|
+
let raw_dir = crate::model::paths::runtime_dir(&ws)
|
|
208
|
+
.join("teams")
|
|
209
|
+
.join("team:legacy-中文");
|
|
210
|
+
std::fs::create_dir_all(&raw_dir).unwrap();
|
|
211
|
+
let snap = serde_json::json!({
|
|
212
|
+
"session_name": "team:legacy-中文",
|
|
213
|
+
"team_owner": {"pane_id": "%8", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
214
|
+
"leader_receiver": {"pane_id": "%8", "owner_epoch": 2},
|
|
215
|
+
});
|
|
216
|
+
std::fs::write(raw_dir.join("state.json"), serde_json::to_string(&snap).unwrap()).unwrap();
|
|
217
|
+
|
|
218
|
+
let d = detect_dual_state_divergence(&ws, &state)
|
|
219
|
+
.unwrap()
|
|
220
|
+
.expect("legacy raw snapshot fallback must preserve old teams");
|
|
221
|
+
assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
|
|
222
|
+
assert_eq!(d["team_owner_pane"], serde_json::json!("%8"));
|
|
223
|
+
assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
|
|
224
|
+
assert_eq!(d["team_receiver_pane"], serde_json::json!("%8"));
|
|
225
|
+
}
|
|
226
|
+
|
|
148
227
|
// detect_dual_state_divergence:无 session_name → None(__init__.py:560-561);unimplemented → RED。
|
|
149
228
|
#[test]
|
|
150
229
|
fn detect_dual_state_divergence_none_without_session_name() {
|
|
@@ -16,14 +16,15 @@ pub fn save_team_runtime_snapshot(
|
|
|
16
16
|
.get("session_name")
|
|
17
17
|
.and_then(|v| v.as_str())
|
|
18
18
|
.ok_or_else(|| LifecycleError::StatePersist("session_name is required".to_string()))?;
|
|
19
|
-
let safe = safe_snapshot_name(session_name);
|
|
20
19
|
// golden restart/snapshot.py:46-47 — team_runtime_snapshot_dir = runtime_dir(workspace)/teams/<safe>,
|
|
21
20
|
// and paths.py:25-26 runtime_dir = workspace/.team/runtime. Use the crate path helper so the snapshot
|
|
22
21
|
// lands at <ws>/.team/runtime/teams/<safe>, matching golden AND the rest of the crate (not the
|
|
23
22
|
// ".team"-less <ws>/runtime/teams that the original port wrote).
|
|
24
|
-
let
|
|
23
|
+
let path = team_snapshot_path(workspace, session_name);
|
|
24
|
+
let dir = path
|
|
25
|
+
.parent()
|
|
26
|
+
.ok_or_else(|| LifecycleError::StatePersist("snapshot path has no parent".to_string()))?;
|
|
25
27
|
fs::create_dir_all(&dir).map_err(|e| persist_err("create snapshot dir", &e))?;
|
|
26
|
-
let path = dir.join("state.json");
|
|
27
28
|
let tmp = dir.join("state.json.tmp");
|
|
28
29
|
let data = serde_json::to_vec_pretty(state)
|
|
29
30
|
.map_err(|e| LifecycleError::StatePersist(format!("serialize snapshot: {e}")))?;
|
|
@@ -32,6 +33,13 @@ pub fn save_team_runtime_snapshot(
|
|
|
32
33
|
Ok(path)
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
pub fn team_snapshot_path(workspace: &Path, session_name: &str) -> PathBuf {
|
|
37
|
+
crate::model::paths::runtime_dir(workspace)
|
|
38
|
+
.join("teams")
|
|
39
|
+
.join(safe_snapshot_name(session_name))
|
|
40
|
+
.join("state.json")
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
fn persist_err(action: &str, err: &io::Error) -> LifecycleError {
|
|
36
44
|
LifecycleError::StatePersist(format!("{action}: {err}"))
|
|
37
45
|
}
|
|
@@ -213,8 +213,7 @@ fn spawn_agents(
|
|
|
213
213
|
safety,
|
|
214
214
|
)?;
|
|
215
215
|
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
216
|
-
let mcp_team_id =
|
|
217
|
-
runtime_active_team_key_for_spawn(workspace, spec_path, spec, session_name);
|
|
216
|
+
let mcp_team_id = runtime_team_key_for_spec(spec_path, spec, session_name);
|
|
218
217
|
let mcp_config = adapter
|
|
219
218
|
.mcp_config(auth_mode)
|
|
220
219
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
@@ -724,8 +723,7 @@ fn persist_spawn_agent_state(
|
|
|
724
723
|
} else {
|
|
725
724
|
serde_json::json!({"agents": {}})
|
|
726
725
|
};
|
|
727
|
-
let team_id =
|
|
728
|
-
.unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name));
|
|
726
|
+
let team_id = runtime_team_key_for_spec(spec_path, spec, session_name);
|
|
729
727
|
let worker_tmux_socket = launched_worker_tmux_socket(transport, workspace);
|
|
730
728
|
drop_worker_pane_seeded_owner(&mut state, &team_id, started, worker_tmux_socket.as_deref());
|
|
731
729
|
// Only persist running state for agents whose spawn still has a live target.
|
|
@@ -2330,18 +2328,6 @@ fn spec_team_id(spec: &Value) -> Option<String> {
|
|
|
2330
2328
|
.or_else(|| spec.get("name").and_then(Value::as_str).map(str::to_string))
|
|
2331
2329
|
}
|
|
2332
2330
|
|
|
2333
|
-
fn runtime_active_team_key_for_spawn(
|
|
2334
|
-
workspace: &Path,
|
|
2335
|
-
spec_path: &Path,
|
|
2336
|
-
spec: &Value,
|
|
2337
|
-
session_name: &SessionName,
|
|
2338
|
-
) -> String {
|
|
2339
|
-
load_runtime_state(workspace)
|
|
2340
|
-
.ok()
|
|
2341
|
-
.and_then(|state| explicit_active_team_key(&state))
|
|
2342
|
-
.unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name))
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
2331
|
fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
|
|
2346
2332
|
state
|
|
2347
2333
|
.get("active_team_key")
|
|
@@ -203,8 +203,42 @@ pub fn deliver_pending_message(
|
|
|
203
203
|
let delivery_transport =
|
|
204
204
|
delivery_transport_for_recipient(workspace, transport, state, &message.recipient);
|
|
205
205
|
let transport = delivery_transport.as_transport();
|
|
206
|
+
// U1-B: probe `list_targets()` ONCE per delivery and DEFER on Err. The prior
|
|
207
|
+
// `.unwrap_or_default()` coerced server jitter (Err = subprocess fork failed)
|
|
208
|
+
// to an empty vec, and the downstream cache-reuse branch
|
|
209
|
+
// (`live_targets.is_empty() && !known_dead`) would then inject into a
|
|
210
|
+
// never-validated cached pane on every tmux hiccup. Err is OBSERVED here as
|
|
211
|
+
// jitter and the delivery is deferred (status stays `target_resolved`, no
|
|
212
|
+
// inject attempted) so the next tick gets a fresh probe.
|
|
213
|
+
let live_targets = match transport.list_targets() {
|
|
214
|
+
Ok(targets) => targets,
|
|
215
|
+
Err(err) => {
|
|
216
|
+
let reason = format!("list_targets_server_jitter:{err}");
|
|
217
|
+
event_log.write(
|
|
218
|
+
"delivery.deferred_list_targets_jitter",
|
|
219
|
+
serde_json::json!({
|
|
220
|
+
"message_id": message_id,
|
|
221
|
+
"recipient": message.recipient,
|
|
222
|
+
"reason": reason,
|
|
223
|
+
}),
|
|
224
|
+
)?;
|
|
225
|
+
store.mark(message_id, "target_resolved", Some(&reason))?;
|
|
226
|
+
return Ok(DeliveryOutcome {
|
|
227
|
+
ok: false,
|
|
228
|
+
status: DeliveryStatus::Degraded,
|
|
229
|
+
message_status: MessageStatusShadow("target_resolved".to_string()),
|
|
230
|
+
message_id: Some(message_id.to_string()),
|
|
231
|
+
verification: Some(reason),
|
|
232
|
+
stage: None,
|
|
233
|
+
reason: None,
|
|
234
|
+
channel: None,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
206
238
|
// Do not inject queued leader messages into a synthetic "leader" window.
|
|
207
|
-
if message.recipient == "leader"
|
|
239
|
+
if message.recipient == "leader"
|
|
240
|
+
&& !leader_receiver_pane_is_usable(transport, state, &live_targets)
|
|
241
|
+
{
|
|
208
242
|
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
209
243
|
event_log.write(
|
|
210
244
|
"leader_receiver.delivery_blocked",
|
|
@@ -227,7 +261,7 @@ pub fn deliver_pending_message(
|
|
|
227
261
|
channel: Some("rebind_required".to_string()),
|
|
228
262
|
});
|
|
229
263
|
}
|
|
230
|
-
let target = resolve_inject_target(state, &message.recipient, transport);
|
|
264
|
+
let target = resolve_inject_target(state, &message.recipient, transport, &live_targets);
|
|
231
265
|
// Contract B / MUST-10 / N31/N32: physical paste+Enter into a startup trust/update
|
|
232
266
|
// menu is NOT provider delivery — the menu consumes the Enter and the task text
|
|
233
267
|
// is lost (PROBE-2 root-cause). Before injection, peek at the recipient's pane for
|
|
@@ -473,12 +507,24 @@ fn render_message(sender: &str, task_id: Option<&str>, content: &str, message_id
|
|
|
473
507
|
///
|
|
474
508
|
/// Leader delivery uses the bound leader receiver pane. The leader is not a worker agent and
|
|
475
509
|
/// must not fall through to a synthetic `SessionWindow{window="leader"}` target.
|
|
510
|
+
///
|
|
511
|
+
/// `live_targets` is passed in BY THE CALLER (probed ONCE per delivery — see U1-B); this fn is
|
|
512
|
+
/// pure w.r.t. transport state. The caller must DEFER on `list_targets()` Err and never coerce
|
|
513
|
+
/// to an empty vec.
|
|
514
|
+
///
|
|
515
|
+
/// **0.3.24 excision (U1-A real-machine RED v2, macmini fixture res_e4b40473d36f)**:
|
|
516
|
+
/// the wave-2 LEADER drift fallback chain (session+window probe) was removed — see
|
|
517
|
+
/// `.team/artifacts/u1-a-realmachine-v2-fix-or-excise.md` for the root-cause analysis.
|
|
518
|
+
/// Drift now fails loudly as `leader_not_attached` rather than silently selecting a stale
|
|
519
|
+
/// pane. U1-A real-machine fix is deferred to v0.3.25 (writer-shape + projection +
|
|
520
|
+
/// rediscover-writer triad). U1-B jitter defer at try_deliver_message:213-237 and
|
|
521
|
+
/// U1-C-Tail at the startup-prompt peek site are unchanged.
|
|
476
522
|
fn resolve_inject_target(
|
|
477
523
|
state: &serde_json::Value,
|
|
478
524
|
recipient: &str,
|
|
479
525
|
transport: &dyn Transport,
|
|
526
|
+
live_targets: &[PaneInfo],
|
|
480
527
|
) -> Target {
|
|
481
|
-
let live_targets = transport.list_targets().unwrap_or_default();
|
|
482
528
|
if recipient == "leader" {
|
|
483
529
|
if let Some(pane_id) = leader_receiver_pane_id(state) {
|
|
484
530
|
return Target::Pane(PaneId::new(pane_id));
|
|
@@ -507,7 +553,7 @@ fn resolve_inject_target(
|
|
|
507
553
|
return Target::Pane(pane.clone());
|
|
508
554
|
}
|
|
509
555
|
}
|
|
510
|
-
if let Some(live_pane) = live_pane_for_session_window(
|
|
556
|
+
if let Some(live_pane) = live_pane_for_session_window(live_targets, session, window) {
|
|
511
557
|
return Target::Pane(live_pane);
|
|
512
558
|
}
|
|
513
559
|
if let Some(pane) = cached_pane {
|
|
@@ -552,16 +598,21 @@ fn leader_receiver_pane_id(state: &serde_json::Value) -> Option<&str> {
|
|
|
552
598
|
.or_else(|| only_team_entry(state).and_then(leader_receiver_pane_id_in_state))
|
|
553
599
|
}
|
|
554
600
|
|
|
555
|
-
|
|
601
|
+
/// `state` is "usable" for leader delivery when the bound `leader_receiver.pane_id`
|
|
602
|
+
/// is present and alive (in `live_targets` or `liveness` probe returns not-Dead).
|
|
603
|
+
///
|
|
604
|
+
/// **0.3.24 excision (U1-A real-machine RED v2)**: the wave-2 session+window drift
|
|
605
|
+
/// fallback was removed. Drift now fails loudly as `leader_not_attached` —
|
|
606
|
+
/// see resolve_inject_target and `.team/artifacts/u1-a-realmachine-v2-fix-or-excise.md`.
|
|
607
|
+
fn leader_receiver_pane_is_usable(
|
|
608
|
+
transport: &dyn Transport,
|
|
609
|
+
state: &serde_json::Value,
|
|
610
|
+
live_targets: &[PaneInfo],
|
|
611
|
+
) -> bool {
|
|
556
612
|
let Some(pane_id) = leader_receiver_pane_id(state) else {
|
|
557
613
|
return false;
|
|
558
614
|
};
|
|
559
|
-
if
|
|
560
|
-
.list_targets()
|
|
561
|
-
.unwrap_or_default()
|
|
562
|
-
.iter()
|
|
563
|
-
.any(|target| target.pane_id.as_str() == pane_id)
|
|
564
|
-
{
|
|
615
|
+
if live_targets.iter().any(|target| target.pane_id.as_str() == pane_id) {
|
|
565
616
|
return true;
|
|
566
617
|
}
|
|
567
618
|
!matches!(
|
|
@@ -956,8 +1007,16 @@ fn recipient_pane_has_actionable_startup_prompt(
|
|
|
956
1007
|
if matches!(startup_prompts, Some("handled" | "complete")) {
|
|
957
1008
|
return false;
|
|
958
1009
|
}
|
|
1010
|
+
// U1-C wave-2: Tail(80) instead of Full. The peek is the DELIVERY-SITE pre-check
|
|
1011
|
+
// (NOT the startup-prompts dismissal phase, which legitimately needs the full
|
|
1012
|
+
// scrollback to anchor recency). Limiting to the visible screen avoids matching
|
|
1013
|
+
// residual already-answered trust modals that scrolled off — while a live trust
|
|
1014
|
+
// modal would still appear in the last 80 lines because Codex pins it to the
|
|
1015
|
+
// visible region until dismissed. Pair this with the U1-C recency guard in
|
|
1016
|
+
// `has_actionable_trust_shape`: either fix alone covers the symptom; both
|
|
1017
|
+
// together defend against both scrollback-residue AND pre-render pathologies.
|
|
959
1018
|
let captured = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
960
|
-
transport.capture(target, crate::transport::CaptureRange::
|
|
1019
|
+
transport.capture(target, crate::transport::CaptureRange::Tail(80))
|
|
961
1020
|
})) {
|
|
962
1021
|
Ok(Ok(captured)) => captured.text,
|
|
963
1022
|
_ => return false,
|
|
@@ -1351,27 +1410,15 @@ pub(crate) fn normalize_owner_team_id_rows(
|
|
|
1351
1410
|
if requested == canonical {
|
|
1352
1411
|
return Ok(());
|
|
1353
1412
|
}
|
|
1354
|
-
let store = MessageStore::open(workspace)?;
|
|
1355
|
-
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
1356
|
-
for table in [
|
|
1357
|
-
"messages",
|
|
1358
|
-
"results",
|
|
1359
|
-
"scheduled_events",
|
|
1360
|
-
"agent_health",
|
|
1361
|
-
"result_watchers",
|
|
1362
|
-
"leader_notification_log",
|
|
1363
|
-
] {
|
|
1364
|
-
let sql =
|
|
1365
|
-
format!("update or ignore {table} set owner_team_id = ?1 where owner_team_id = ?2");
|
|
1366
|
-
conn.execute(&sql, params![canonical, requested])?;
|
|
1367
|
-
}
|
|
1368
1413
|
if let Some(event_log) = event_log {
|
|
1369
1414
|
event_log.write(
|
|
1370
|
-
"owner_team_id.
|
|
1415
|
+
"owner_team_id.compatibility_alias_detected",
|
|
1371
1416
|
serde_json::json!({
|
|
1372
1417
|
"requested_owner_team_id": requested,
|
|
1373
1418
|
"canonical_owner_team_id": canonical,
|
|
1374
1419
|
"message_id": message_id,
|
|
1420
|
+
"action": "read_only_no_db_update",
|
|
1421
|
+
"workspace": workspace.to_string_lossy().to_string(),
|
|
1375
1422
|
}),
|
|
1376
1423
|
)?;
|
|
1377
1424
|
}
|