@team-agent/installer 0.5.22 → 0.5.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/cli/diagnose.rs +22 -0
- package/crates/team-agent/src/cli/mod.rs +16 -2
- package/crates/team-agent/src/cli/send.rs +8 -0
- package/crates/team-agent/src/cli/status_port.rs +16 -2
- package/crates/team-agent/src/coordinator/health.rs +157 -13
- package/crates/team-agent/src/coordinator/types.rs +30 -0
- package/crates/team-agent/src/leader/lease.rs +22 -33
- package/crates/team-agent/src/leader/rediscover/tests.rs +6 -1
- package/crates/team-agent/src/leader/tests/lease_api.rs +40 -22
- package/crates/team-agent/src/leader/tests/lease_claim.rs +12 -29
- package/crates/team-agent/src/lifecycle/helpers.rs +40 -7
- package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -6
- package/crates/team-agent/src/lifecycle/restart/remove.rs +6 -11
- package/crates/team-agent/src/mcp_server/helpers.rs +86 -27
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +37 -0
- package/crates/team-agent/src/mcp_server/tools.rs +68 -37
- package/crates/team-agent/src/messaging/send.rs +25 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -205,6 +205,14 @@ fn coordinator_issue_id(
|
|
|
205
205
|
}
|
|
206
206
|
crate::coordinator::CoordinatorHealthStatus::Running => {
|
|
207
207
|
if !health.metadata_ok {
|
|
208
|
+
if health.service_available
|
|
209
|
+
&& matches!(
|
|
210
|
+
health.binary_identity_relation,
|
|
211
|
+
crate::coordinator::CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
|
|
212
|
+
)
|
|
213
|
+
{
|
|
214
|
+
return None;
|
|
215
|
+
}
|
|
208
216
|
return match health.metadata_mismatch_reason.as_deref() {
|
|
209
217
|
Some(
|
|
210
218
|
"binary_identity_missing"
|
|
@@ -237,6 +245,11 @@ fn coordinator_issue_value(
|
|
|
237
245
|
"pid": health.pid.map(|pid| pid.get()),
|
|
238
246
|
"metadata_ok": health.metadata_ok,
|
|
239
247
|
"metadata_mismatch_reason": health.metadata_mismatch_reason.clone(),
|
|
248
|
+
"process_running": health.process_running,
|
|
249
|
+
"wire_metadata_ok": health.wire_metadata_ok,
|
|
250
|
+
"binary_identity_ok": health.binary_identity_ok,
|
|
251
|
+
"binary_identity_relation": health.binary_identity_relation.as_str(),
|
|
252
|
+
"service_available": health.service_available,
|
|
240
253
|
"binary_path": health.current_binary_identity.binary_path.clone(),
|
|
241
254
|
"binary_version": health.current_binary_identity.binary_version.clone(),
|
|
242
255
|
"schema_ok": health.schema.ok,
|
|
@@ -378,6 +391,15 @@ mod tests {
|
|
|
378
391
|
pid: Some(crate::coordinator::Pid::new(std::process::id())),
|
|
379
392
|
metadata: None,
|
|
380
393
|
metadata_ok: metadata_mismatch_reason.is_none(),
|
|
394
|
+
process_running: matches!(status, crate::coordinator::CoordinatorHealthStatus::Running),
|
|
395
|
+
wire_metadata_ok: metadata_mismatch_reason.is_none(),
|
|
396
|
+
binary_identity_ok: metadata_mismatch_reason.is_none(),
|
|
397
|
+
binary_identity_relation: crate::coordinator::CoordinatorBinaryIdentityRelation::Same,
|
|
398
|
+
service_available: matches!(
|
|
399
|
+
status,
|
|
400
|
+
crate::coordinator::CoordinatorHealthStatus::Running
|
|
401
|
+
) && metadata_mismatch_reason.is_none()
|
|
402
|
+
&& schema_ok,
|
|
381
403
|
metadata_mismatch_reason: metadata_mismatch_reason.map(ToString::to_string),
|
|
382
404
|
current_binary_identity: crate::coordinator::CoordinatorBinaryIdentity {
|
|
383
405
|
binary_path: "/current/team-agent".to_string(),
|
|
@@ -3947,7 +3947,9 @@ pub mod diagnose_port {
|
|
|
3947
3947
|
}
|
|
3948
3948
|
|
|
3949
3949
|
fn coordinator_health_value(health: crate::coordinator::HealthReport) -> Value {
|
|
3950
|
-
|
|
3950
|
+
let expose_binary_drift = health.service_available && !health.metadata_ok;
|
|
3951
|
+
let binary_identity_relation = health.binary_identity_relation.as_str();
|
|
3952
|
+
let mut value = json!({
|
|
3951
3953
|
"ok": health.ok,
|
|
3952
3954
|
"status": coordinator_status_wire(health.status),
|
|
3953
3955
|
"pid": health.pid.map(|p| p.get()),
|
|
@@ -3969,7 +3971,19 @@ pub mod diagnose_port {
|
|
|
3969
3971
|
"schema": {
|
|
3970
3972
|
"message_store_schema_version": health.schema.schema_version,
|
|
3971
3973
|
},
|
|
3972
|
-
})
|
|
3974
|
+
});
|
|
3975
|
+
if expose_binary_drift {
|
|
3976
|
+
if let Some(obj) = value.as_object_mut() {
|
|
3977
|
+
obj.insert("wire_metadata_ok".to_string(), Value::Bool(true));
|
|
3978
|
+
obj.insert("binary_identity_ok".to_string(), Value::Bool(false));
|
|
3979
|
+
obj.insert(
|
|
3980
|
+
"binary_identity_relation".to_string(),
|
|
3981
|
+
Value::String(binary_identity_relation.to_string()),
|
|
3982
|
+
);
|
|
3983
|
+
obj.insert("service_available".to_string(), Value::Bool(true));
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
value
|
|
3973
3987
|
}
|
|
3974
3988
|
|
|
3975
3989
|
fn coordinator_status_wire(
|
|
@@ -239,6 +239,14 @@ fn loud_ensure_coordinator(
|
|
|
239
239
|
if previous.ok {
|
|
240
240
|
return Ok(None);
|
|
241
241
|
}
|
|
242
|
+
if previous.service_available
|
|
243
|
+
&& matches!(
|
|
244
|
+
previous.binary_identity_relation,
|
|
245
|
+
crate::coordinator::CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
|
|
246
|
+
)
|
|
247
|
+
{
|
|
248
|
+
return Ok(None);
|
|
249
|
+
}
|
|
242
250
|
let previous_status = coordinator_health_status_wire(previous.status).to_string();
|
|
243
251
|
let start = crate::coordinator::start_coordinator_with_team(
|
|
244
252
|
&workspace,
|
|
@@ -1143,7 +1143,9 @@ use rusqlite::params;
|
|
|
1143
1143
|
}
|
|
1144
1144
|
|
|
1145
1145
|
fn coordinator_health_value(health: crate::coordinator::HealthReport) -> Value {
|
|
1146
|
-
|
|
1146
|
+
let expose_binary_drift = health.service_available && !health.metadata_ok;
|
|
1147
|
+
let binary_identity_relation = health.binary_identity_relation.as_str();
|
|
1148
|
+
let mut value = json!({
|
|
1147
1149
|
"ok": health.ok,
|
|
1148
1150
|
"status": coordinator_status_wire(health.status),
|
|
1149
1151
|
"pid": health.pid.map(|p| p.get()),
|
|
@@ -1165,7 +1167,19 @@ use rusqlite::params;
|
|
|
1165
1167
|
"schema": {
|
|
1166
1168
|
"message_store_schema_version": health.schema.schema_version,
|
|
1167
1169
|
},
|
|
1168
|
-
})
|
|
1170
|
+
});
|
|
1171
|
+
if expose_binary_drift {
|
|
1172
|
+
if let Some(obj) = value.as_object_mut() {
|
|
1173
|
+
obj.insert("wire_metadata_ok".to_string(), Value::Bool(true));
|
|
1174
|
+
obj.insert("binary_identity_ok".to_string(), Value::Bool(false));
|
|
1175
|
+
obj.insert(
|
|
1176
|
+
"binary_identity_relation".to_string(),
|
|
1177
|
+
Value::String(binary_identity_relation.to_string()),
|
|
1178
|
+
);
|
|
1179
|
+
obj.insert("service_available".to_string(), Value::Bool(true));
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
value
|
|
1169
1183
|
}
|
|
1170
1184
|
|
|
1171
1185
|
fn coordinator_status_wire(status: crate::coordinator::CoordinatorHealthStatus) -> &'static str {
|
|
@@ -13,10 +13,10 @@ use thiserror::Error;
|
|
|
13
13
|
use crate::message_store::MessageStore;
|
|
14
14
|
|
|
15
15
|
use super::types::{
|
|
16
|
-
CoordinatorBinaryIdentity,
|
|
17
|
-
CoordinatorMetadataMismatchReason, HealthReport, MetadataSource, Pid,
|
|
18
|
-
SchemaHealth, StartError, StartOutcome, StartReport, StopError, StopOutcome,
|
|
19
|
-
WatchCursor, WorkspacePath, PROTOCOL_VERSION, ROTATION_MARKER,
|
|
16
|
+
CoordinatorBinaryIdentity, CoordinatorBinaryIdentityRelation, CoordinatorHealthStatus,
|
|
17
|
+
CoordinatorMetadata, CoordinatorMetadataMismatchReason, HealthReport, MetadataSource, Pid,
|
|
18
|
+
SchemaError, SchemaHealth, StartError, StartOutcome, StartReport, StopError, StopOutcome,
|
|
19
|
+
StopReport, WatchCursor, WorkspacePath, PROTOCOL_VERSION, ROTATION_MARKER,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
// ===========================================================================
|
|
@@ -40,21 +40,30 @@ pub fn coordinator_health(workspace: &WorkspacePath) -> HealthReport {
|
|
|
40
40
|
None => CoordinatorHealthStatus::Missing,
|
|
41
41
|
};
|
|
42
42
|
let metadata = read_coordinator_metadata(workspace);
|
|
43
|
-
let
|
|
44
|
-
.map(|p|
|
|
45
|
-
metadata.as_ref(),
|
|
46
|
-
p,
|
|
47
|
-
¤t_binary_identity,
|
|
48
|
-
))
|
|
43
|
+
let wire_metadata_mismatch = pid
|
|
44
|
+
.map(|p| coordinator_wire_metadata_mismatch_reason(metadata.as_ref(), p))
|
|
49
45
|
.unwrap_or(Some(CoordinatorMetadataMismatchReason::MetadataMissing));
|
|
50
|
-
let
|
|
51
|
-
|
|
46
|
+
let binary_identity_mismatch =
|
|
47
|
+
coordinator_binary_identity_mismatch_reason(metadata.as_ref(), ¤t_binary_identity);
|
|
48
|
+
let wire_metadata_ok = wire_metadata_mismatch.is_none();
|
|
49
|
+
let binary_identity_ok = binary_identity_mismatch.is_none();
|
|
50
|
+
let metadata_mismatch = wire_metadata_mismatch.or(binary_identity_mismatch);
|
|
51
|
+
let metadata_ok = wire_metadata_ok && binary_identity_ok;
|
|
52
|
+
let process_running = matches!(status, CoordinatorHealthStatus::Running);
|
|
53
|
+
let binary_identity_relation =
|
|
54
|
+
coordinator_binary_identity_relation(metadata.as_ref(), ¤t_binary_identity);
|
|
55
|
+
let service_available = process_running && wire_metadata_ok && schema.ok;
|
|
52
56
|
HealthReport {
|
|
53
|
-
ok:
|
|
57
|
+
ok: process_running && metadata_ok && schema.ok,
|
|
54
58
|
status,
|
|
55
59
|
pid,
|
|
56
60
|
metadata,
|
|
57
61
|
metadata_ok,
|
|
62
|
+
process_running,
|
|
63
|
+
wire_metadata_ok,
|
|
64
|
+
binary_identity_ok,
|
|
65
|
+
binary_identity_relation,
|
|
66
|
+
service_available,
|
|
58
67
|
metadata_mismatch_reason: metadata_mismatch.map(|reason| reason.as_str().to_string()),
|
|
59
68
|
current_binary_identity,
|
|
60
69
|
schema,
|
|
@@ -109,6 +118,62 @@ pub fn start_coordinator_with_team(
|
|
|
109
118
|
action: health.schema.action,
|
|
110
119
|
});
|
|
111
120
|
}
|
|
121
|
+
if matches!(health.status, CoordinatorHealthStatus::Running) && !health.wire_metadata_ok {
|
|
122
|
+
return Ok(StartReport {
|
|
123
|
+
ok: false,
|
|
124
|
+
pid: health.pid,
|
|
125
|
+
status: StartOutcome::RestartIncompatibleStopFailed,
|
|
126
|
+
previous_pid: health.pid,
|
|
127
|
+
binary_path: Some(identity.binary_path),
|
|
128
|
+
binary_version: Some(identity.binary_version),
|
|
129
|
+
rotation_reason: health.metadata_mismatch_reason,
|
|
130
|
+
log: None,
|
|
131
|
+
schema_error: None,
|
|
132
|
+
action: Some(
|
|
133
|
+
"refusing to rotate a coordinator with incompatible protocol or schema metadata"
|
|
134
|
+
.to_string(),
|
|
135
|
+
),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if matches!(health.status, CoordinatorHealthStatus::Running)
|
|
139
|
+
&& health.wire_metadata_ok
|
|
140
|
+
&& !health.binary_identity_ok
|
|
141
|
+
&& matches!(
|
|
142
|
+
health.binary_identity_relation,
|
|
143
|
+
CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
|
|
144
|
+
)
|
|
145
|
+
{
|
|
146
|
+
crate::event_log::EventLog::new(workspace.as_path()).write(
|
|
147
|
+
"coordinator.newer_daemon_preserved",
|
|
148
|
+
serde_json::json!({
|
|
149
|
+
"pid": health.pid.map(|pid| pid.get()),
|
|
150
|
+
"binary_identity_relation": health.binary_identity_relation.as_str(),
|
|
151
|
+
"reason": "daemon_newer_than_caller",
|
|
152
|
+
"daemon_binary_path": health
|
|
153
|
+
.metadata
|
|
154
|
+
.as_ref()
|
|
155
|
+
.and_then(|metadata| metadata.binary_path.clone()),
|
|
156
|
+
"daemon_binary_version": health
|
|
157
|
+
.metadata
|
|
158
|
+
.as_ref()
|
|
159
|
+
.and_then(|metadata| metadata.binary_version.clone()),
|
|
160
|
+
"caller_binary_path": identity.binary_path.clone(),
|
|
161
|
+
"caller_binary_version": identity.binary_version.clone(),
|
|
162
|
+
}),
|
|
163
|
+
)?;
|
|
164
|
+
return Ok(StartReport {
|
|
165
|
+
ok: true,
|
|
166
|
+
pid: health.pid,
|
|
167
|
+
status: StartOutcome::AlreadyRunning,
|
|
168
|
+
previous_pid: None,
|
|
169
|
+
binary_path: Some(identity.binary_path),
|
|
170
|
+
binary_version: Some(identity.binary_version),
|
|
171
|
+
rotation_reason: Some("daemon_newer_than_caller".to_string()),
|
|
172
|
+
log: Some(coordinator_log_path(workspace)),
|
|
173
|
+
schema_error: None,
|
|
174
|
+
action: None,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
112
177
|
let rotation_reason = if matches!(health.status, CoordinatorHealthStatus::Running)
|
|
113
178
|
&& !health.metadata_ok
|
|
114
179
|
{
|
|
@@ -580,6 +645,13 @@ pub fn read_coordinator_metadata(workspace: &WorkspacePath) -> Option<Coordinato
|
|
|
580
645
|
}
|
|
581
646
|
|
|
582
647
|
pub fn current_coordinator_binary_identity() -> CoordinatorBinaryIdentity {
|
|
648
|
+
if let Ok(raw) = std::env::var("TEAM_AGENT_TEST_CALLER_BINARY_IDENTITY") {
|
|
649
|
+
if let Ok(identity) = serde_json::from_str::<CoordinatorBinaryIdentity>(&raw) {
|
|
650
|
+
if !identity.binary_path.is_empty() && !identity.binary_version.is_empty() {
|
|
651
|
+
return identity;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
583
655
|
let binary_path = std::env::current_exe()
|
|
584
656
|
.map(|path| path.canonicalize().unwrap_or(path))
|
|
585
657
|
.map(|path| path.to_string_lossy().into_owned())
|
|
@@ -608,6 +680,14 @@ fn coordinator_metadata_mismatch_reason_with_identity(
|
|
|
608
680
|
metadata: Option<&CoordinatorMetadata>,
|
|
609
681
|
pid: Pid,
|
|
610
682
|
identity: &CoordinatorBinaryIdentity,
|
|
683
|
+
) -> Option<CoordinatorMetadataMismatchReason> {
|
|
684
|
+
coordinator_wire_metadata_mismatch_reason(metadata, pid)
|
|
685
|
+
.or_else(|| coordinator_binary_identity_mismatch_reason(metadata, identity))
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
fn coordinator_wire_metadata_mismatch_reason(
|
|
689
|
+
metadata: Option<&CoordinatorMetadata>,
|
|
690
|
+
pid: Pid,
|
|
611
691
|
) -> Option<CoordinatorMetadataMismatchReason> {
|
|
612
692
|
let Some(metadata) = metadata else {
|
|
613
693
|
return Some(CoordinatorMetadataMismatchReason::MetadataMissing);
|
|
@@ -621,6 +701,16 @@ fn coordinator_metadata_mismatch_reason_with_identity(
|
|
|
621
701
|
if metadata.message_store_schema_version != crate::db::schema::SCHEMA_VERSION {
|
|
622
702
|
return Some(CoordinatorMetadataMismatchReason::MessageStoreSchemaVersionMismatch);
|
|
623
703
|
}
|
|
704
|
+
None
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
fn coordinator_binary_identity_mismatch_reason(
|
|
708
|
+
metadata: Option<&CoordinatorMetadata>,
|
|
709
|
+
identity: &CoordinatorBinaryIdentity,
|
|
710
|
+
) -> Option<CoordinatorMetadataMismatchReason> {
|
|
711
|
+
let Some(metadata) = metadata else {
|
|
712
|
+
return Some(CoordinatorMetadataMismatchReason::MetadataMissing);
|
|
713
|
+
};
|
|
624
714
|
let Some(binary_version) = metadata.binary_version.as_deref().filter(|value| !value.is_empty())
|
|
625
715
|
else {
|
|
626
716
|
return Some(CoordinatorMetadataMismatchReason::BinaryIdentityMissing);
|
|
@@ -638,6 +728,60 @@ fn coordinator_metadata_mismatch_reason_with_identity(
|
|
|
638
728
|
None
|
|
639
729
|
}
|
|
640
730
|
|
|
731
|
+
fn coordinator_binary_identity_relation(
|
|
732
|
+
metadata: Option<&CoordinatorMetadata>,
|
|
733
|
+
identity: &CoordinatorBinaryIdentity,
|
|
734
|
+
) -> CoordinatorBinaryIdentityRelation {
|
|
735
|
+
let Some(metadata) = metadata else {
|
|
736
|
+
return CoordinatorBinaryIdentityRelation::Unknown;
|
|
737
|
+
};
|
|
738
|
+
let Some(daemon_version) = metadata
|
|
739
|
+
.binary_version
|
|
740
|
+
.as_deref()
|
|
741
|
+
.filter(|value| !value.is_empty())
|
|
742
|
+
else {
|
|
743
|
+
return CoordinatorBinaryIdentityRelation::Unknown;
|
|
744
|
+
};
|
|
745
|
+
match compare_version_strings(daemon_version, &identity.binary_version) {
|
|
746
|
+
Some(std::cmp::Ordering::Greater) => {
|
|
747
|
+
return CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller;
|
|
748
|
+
}
|
|
749
|
+
Some(std::cmp::Ordering::Less) => {
|
|
750
|
+
return CoordinatorBinaryIdentityRelation::CallerNewerThanDaemon;
|
|
751
|
+
}
|
|
752
|
+
Some(std::cmp::Ordering::Equal) => {}
|
|
753
|
+
None => return CoordinatorBinaryIdentityRelation::Unknown,
|
|
754
|
+
}
|
|
755
|
+
let Some(daemon_path) = metadata
|
|
756
|
+
.binary_path
|
|
757
|
+
.as_deref()
|
|
758
|
+
.filter(|value| !value.is_empty())
|
|
759
|
+
else {
|
|
760
|
+
return CoordinatorBinaryIdentityRelation::Unknown;
|
|
761
|
+
};
|
|
762
|
+
if binary_path_matches_current_identity(daemon_path, &identity.binary_path) {
|
|
763
|
+
CoordinatorBinaryIdentityRelation::Same
|
|
764
|
+
} else {
|
|
765
|
+
CoordinatorBinaryIdentityRelation::SameVersionPathMismatch
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
fn compare_version_strings(left: &str, right: &str) -> Option<std::cmp::Ordering> {
|
|
770
|
+
let left = parse_numeric_version(left)?;
|
|
771
|
+
let right = parse_numeric_version(right)?;
|
|
772
|
+
Some(left.cmp(&right))
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
fn parse_numeric_version(value: &str) -> Option<Vec<u64>> {
|
|
776
|
+
if value.is_empty() {
|
|
777
|
+
return None;
|
|
778
|
+
}
|
|
779
|
+
value
|
|
780
|
+
.split('.')
|
|
781
|
+
.map(|part| part.parse::<u64>().ok())
|
|
782
|
+
.collect()
|
|
783
|
+
}
|
|
784
|
+
|
|
641
785
|
fn binary_path_matches_current_identity(metadata_path: &str, identity_path: &str) -> bool {
|
|
642
786
|
if metadata_path == identity_path {
|
|
643
787
|
return true;
|
|
@@ -189,6 +189,29 @@ pub struct CoordinatorBinaryIdentity {
|
|
|
189
189
|
pub binary_version: String,
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/// Caller-relative daemon binary identity relation.
|
|
193
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
194
|
+
#[serde(rename_all = "snake_case")]
|
|
195
|
+
pub enum CoordinatorBinaryIdentityRelation {
|
|
196
|
+
Same,
|
|
197
|
+
CallerNewerThanDaemon,
|
|
198
|
+
DaemonNewerThanCaller,
|
|
199
|
+
SameVersionPathMismatch,
|
|
200
|
+
Unknown,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
impl CoordinatorBinaryIdentityRelation {
|
|
204
|
+
pub fn as_str(self) -> &'static str {
|
|
205
|
+
match self {
|
|
206
|
+
Self::Same => "same",
|
|
207
|
+
Self::CallerNewerThanDaemon => "caller_newer_than_daemon",
|
|
208
|
+
Self::DaemonNewerThanCaller => "daemon_newer_than_caller",
|
|
209
|
+
Self::SameVersionPathMismatch => "same_version_path_mismatch",
|
|
210
|
+
Self::Unknown => "unknown",
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
/// Stable machine-readable reason for coordinator metadata rejection.
|
|
193
216
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
194
217
|
#[serde(rename_all = "snake_case")]
|
|
@@ -285,6 +308,13 @@ pub struct HealthReport {
|
|
|
285
308
|
pub metadata: Option<CoordinatorMetadata>,
|
|
286
309
|
/// 三元全等(`coordinator_metadata_ok`,`metadata.py:37-43`)。
|
|
287
310
|
pub metadata_ok: bool,
|
|
311
|
+
pub process_running: bool,
|
|
312
|
+
/// pid/protocol/schema match; excludes daemon binary identity.
|
|
313
|
+
pub wire_metadata_ok: bool,
|
|
314
|
+
pub binary_identity_ok: bool,
|
|
315
|
+
pub binary_identity_relation: CoordinatorBinaryIdentityRelation,
|
|
316
|
+
/// A live daemon that can process this workspace's queue.
|
|
317
|
+
pub service_available: bool,
|
|
288
318
|
pub metadata_mismatch_reason: Option<String>,
|
|
289
319
|
pub current_binary_identity: CoordinatorBinaryIdentity,
|
|
290
320
|
pub schema: SchemaHealth,
|
|
@@ -1615,33 +1615,14 @@ fn state_owner(state: &Value) -> Option<TeamOwner> {
|
|
|
1615
1615
|
.and_then(|v| serde_json::from_value(v).ok())
|
|
1616
1616
|
}
|
|
1617
1617
|
|
|
1618
|
-
/// `_write_lease_dual_state`
|
|
1619
|
-
///
|
|
1618
|
+
/// `_write_lease_dual_state` — Foundation-0 F0-2: the historical dual
|
|
1619
|
+
/// write to the legacy per-session snapshot has been retired
|
|
1620
|
+
/// (`.team/artifacts/foundation-0-slice-design.md` §§4-5). This helper
|
|
1621
|
+
/// now persists ONLY the canonical root state; retaining the public
|
|
1622
|
+
/// name for 0.5.x call-site stability. The B0 legacy snapshot is
|
|
1623
|
+
/// diagnostic-only via `lifecycle::save_team_runtime_snapshot`.
|
|
1620
1624
|
pub fn write_lease_dual_state(workspace: &Path, state: &Value) -> Result<(), LeaderError> {
|
|
1621
1625
|
crate::state::persist::save_runtime_state(workspace, state)?;
|
|
1622
|
-
if let Some(session_name) = state.get("session_name").and_then(Value::as_str) {
|
|
1623
|
-
write_team_snapshot_atomic(workspace, session_name, state)?;
|
|
1624
|
-
}
|
|
1625
|
-
Ok(())
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
fn write_team_snapshot_atomic(
|
|
1629
|
-
workspace: &Path,
|
|
1630
|
-
session_name: &str,
|
|
1631
|
-
state: &Value,
|
|
1632
|
-
) -> Result<(), LeaderError> {
|
|
1633
|
-
let snap_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name);
|
|
1634
|
-
let parent = snap_path
|
|
1635
|
-
.parent()
|
|
1636
|
-
.ok_or_else(|| LeaderError::Validation("team snapshot path has no parent".to_string()))?;
|
|
1637
|
-
std::fs::create_dir_all(parent)?;
|
|
1638
|
-
let tmp = parent.join(format!("state.json.tmp-{}", std::process::id()));
|
|
1639
|
-
let data = serde_json::to_vec_pretty(state)?;
|
|
1640
|
-
std::fs::write(&tmp, data)?;
|
|
1641
|
-
if let Err(error) = std::fs::rename(&tmp, &snap_path) {
|
|
1642
|
-
let _ = std::fs::remove_file(&tmp);
|
|
1643
|
-
return Err(error.into());
|
|
1644
|
-
}
|
|
1645
1626
|
Ok(())
|
|
1646
1627
|
}
|
|
1647
1628
|
|
|
@@ -1769,17 +1750,22 @@ fn compact_team_state_preserving_claim_fields(state: &Value, target_key: &str) -
|
|
|
1769
1750
|
entry
|
|
1770
1751
|
}
|
|
1771
1752
|
|
|
1772
|
-
///
|
|
1773
|
-
///
|
|
1774
|
-
///
|
|
1775
|
-
|
|
1753
|
+
/// Foundation-0 F0-2: reader for legacy per-session snapshot vs
|
|
1754
|
+
/// canonical root state. Diagnostic-only after the dual-write retirement
|
|
1755
|
+
/// (`.team/artifacts/foundation-0-slice-design.md` §§4-5); product
|
|
1756
|
+
/// authority code never consults this — it exists so `status`/`diagnose`
|
|
1757
|
+
/// can surface `legacy_snapshot_stale` when the on-disk sidecar has
|
|
1758
|
+
/// drifted. Every touch of the legacy path constants below is marked
|
|
1759
|
+
/// `B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ` so the RED3 grep guard admits
|
|
1760
|
+
/// them as documented exceptions.
|
|
1761
|
+
pub fn detect_dual_state_divergence( // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: diagnostic-only entry point; no product save/route consumer.
|
|
1776
1762
|
workspace: &Path,
|
|
1777
1763
|
state: &Value,
|
|
1778
1764
|
) -> Result<Option<Value>, LeaderError> {
|
|
1779
1765
|
let Some(session_name) = state.get("session_name").and_then(Value::as_str) else {
|
|
1780
1766
|
return Ok(None);
|
|
1781
1767
|
};
|
|
1782
|
-
let snap_path = readable_team_snapshot_path(workspace, session_name);
|
|
1768
|
+
let snap_path = readable_team_snapshot_path(workspace, session_name); // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: diagnostic legacy-shape path lookup.
|
|
1783
1769
|
if !snap_path.exists() {
|
|
1784
1770
|
return Ok(None);
|
|
1785
1771
|
}
|
|
@@ -1810,16 +1796,19 @@ pub fn detect_dual_state_divergence(
|
|
|
1810
1796
|
"team_receiver_pane": team_receiver_pane,
|
|
1811
1797
|
"workspace_owner_epoch": workspace_epoch,
|
|
1812
1798
|
"team_owner_epoch": team_epoch,
|
|
1799
|
+
"_legacy_snapshot_stale": true,
|
|
1813
1800
|
})))
|
|
1814
1801
|
}
|
|
1815
1802
|
|
|
1816
|
-
fn readable_team_snapshot_path(workspace: &Path, session_name: &str) -> PathBuf {
|
|
1817
|
-
let safe_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name);
|
|
1803
|
+
fn readable_team_snapshot_path(workspace: &Path, session_name: &str) -> PathBuf { // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: diagnostic-only path resolver.
|
|
1804
|
+
let safe_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name); // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: reuses helpers safe legacy path.
|
|
1818
1805
|
if safe_path.exists() {
|
|
1819
1806
|
return safe_path;
|
|
1820
1807
|
}
|
|
1808
|
+
// Raw legacy `runtime/teams` fallback for pre-safe-shape snapshots. // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ
|
|
1809
|
+
let legacy_dir = "teams"; // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ
|
|
1821
1810
|
crate::model::paths::runtime_dir(workspace)
|
|
1822
|
-
.join(
|
|
1811
|
+
.join(legacy_dir)
|
|
1823
1812
|
.join(session_name)
|
|
1824
1813
|
.join("state.json")
|
|
1825
1814
|
}
|
|
@@ -135,7 +135,12 @@ fn try_readopt_writes_owner_receiver_dual_state_and_events() {
|
|
|
135
135
|
assert_eq!(owner_view["claimed_via"], json!("attach-leader"));
|
|
136
136
|
assert_eq!(owner_view["owner_epoch"], json!(4));
|
|
137
137
|
assert_eq!(receiver_view["discovery"], json!("attach_readopt"));
|
|
138
|
-
|
|
138
|
+
// Foundation-0 F0-2: the try_readopt success path no longer side-
|
|
139
|
+
// effects a legacy per-session snapshot; canonical teams.<key> is
|
|
140
|
+
// the sole authority. The legacy sidecar stays absent unless a
|
|
141
|
+
// diagnostic writer explicitly created it
|
|
142
|
+
// (`.team/artifacts/foundation-0-slice-design.md` §§4-5).
|
|
143
|
+
assert!(!crate::model::paths::runtime_dir(&ws)
|
|
139
144
|
.join("teams")
|
|
140
145
|
.join("sess")
|
|
141
146
|
.join("state.json")
|
|
@@ -112,7 +112,15 @@ use super::*;
|
|
|
112
112
|
// 强化:带 session_name 时必须落 BOTH —— workspace state.json + team/<session> snapshot,
|
|
113
113
|
// 两份 team_owner.pane_id / owner_epoch 必须一致(永不分叉)。空 body Ok(()) 会被这里抓。
|
|
114
114
|
#[test]
|
|
115
|
-
|
|
115
|
+
// Foundation-0 F0-2 conversion of the C17 dual-write invariant test:
|
|
116
|
+
// legacy per-session snapshot dual-write has been retired
|
|
117
|
+
// (`.team/artifacts/foundation-0-slice-design.md` §§4-5). The lease
|
|
118
|
+
// helper now persists ONLY the canonical root state; a legacy
|
|
119
|
+
// snapshot is diagnostic-only and never re-appears as a side effect
|
|
120
|
+
// of `write_lease_dual_state`. This test enforces exactly that:
|
|
121
|
+
// canonical root gets the owner tuple, and the legacy per-session
|
|
122
|
+
// path stays absent unless a diagnostic path explicitly writes it.
|
|
123
|
+
fn write_lease_dual_state_persists_canonical_root_only_no_legacy_side_effect() {
|
|
116
124
|
let ws = std::env::temp_dir().join(format!("ta_rs_dual_{}", std::process::id()));
|
|
117
125
|
std::fs::create_dir_all(&ws).unwrap();
|
|
118
126
|
let state = serde_json::json!({
|
|
@@ -121,32 +129,33 @@ use super::*;
|
|
|
121
129
|
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
122
130
|
});
|
|
123
131
|
write_lease_dual_state(&ws, &state).unwrap();
|
|
124
|
-
//
|
|
132
|
+
// Canonical root state.json IS written; F0-2 keeps this as the
|
|
133
|
+
// single product authority.
|
|
125
134
|
let ws_path = crate::state::persist::runtime_state_path(&ws);
|
|
126
135
|
let ws_state: serde_json::Value =
|
|
127
136
|
serde_json::from_str(&std::fs::read_to_string(&ws_path).expect("workspace state.json 必须存在")).unwrap();
|
|
128
137
|
assert_eq!(ws_state["team_owner"]["pane_id"], serde_json::json!("%1"));
|
|
129
138
|
assert_eq!(ws_state["team_owner"]["owner_epoch"], serde_json::json!(2));
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
assert_eq!(
|
|
139
|
-
ws_state["team_owner"]["pane_id"], snap_state["team_owner"]["pane_id"],
|
|
140
|
-
"workspace 与 team snapshot 的 owner pane 不得分叉"
|
|
141
|
-
);
|
|
142
|
-
assert_eq!(
|
|
143
|
-
ws_state["team_owner"]["owner_epoch"], snap_state["team_owner"]["owner_epoch"],
|
|
144
|
-
"workspace 与 team snapshot 的 owner_epoch 不得分叉"
|
|
139
|
+
// Legacy per-session snapshot MUST NOT be a side effect of the
|
|
140
|
+
// lease save path any more — this is the RED2 authority-write
|
|
141
|
+
// guard shape at the unit-test layer.
|
|
142
|
+
let legacy_snap = crate::lifecycle::helpers::team_snapshot_path(&ws, "team-sess");
|
|
143
|
+
assert!(
|
|
144
|
+
!legacy_snap.exists(),
|
|
145
|
+
"F0-2: write_lease_dual_state must not write the legacy per-session snapshot; found {}",
|
|
146
|
+
legacy_snap.display(),
|
|
145
147
|
);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
#[test]
|
|
149
151
|
fn lease_divergence_reads_restart_snapshot_for_special_session_names() {
|
|
152
|
+
// Foundation-0 F0-2 conversion: `write_lease_dual_state` no longer
|
|
153
|
+
// side-effects a legacy snapshot; the divergence detector is a
|
|
154
|
+
// diagnostic reader that fires only when a diagnostic writer
|
|
155
|
+
// (e.g. `lifecycle::save_team_runtime_snapshot`) has produced
|
|
156
|
+
// the sidecar. Sequence exercised here: canonical root save →
|
|
157
|
+
// diagnostic snapshot save with a different pane → divergence
|
|
158
|
+
// reader observes the drift.
|
|
150
159
|
let ws = std::env::temp_dir().join(format!("ta_rs_dual_special_{}", std::process::id()));
|
|
151
160
|
std::fs::create_dir_all(&ws).unwrap();
|
|
152
161
|
let state = serde_json::json!({
|
|
@@ -160,17 +169,21 @@ use super::*;
|
|
|
160
169
|
.join("teams")
|
|
161
170
|
.join("team:proj-中文")
|
|
162
171
|
.join("state.json");
|
|
172
|
+
// F0-2: canonical save must not create either the restart-safe
|
|
173
|
+
// or the raw legacy snapshot as a side effect.
|
|
163
174
|
assert!(
|
|
164
|
-
safe_path.exists(),
|
|
165
|
-
"
|
|
175
|
+
!safe_path.exists(),
|
|
176
|
+
"F0-2: canonical write_lease_dual_state must not create the restart-safe legacy snapshot: {}",
|
|
166
177
|
safe_path.display()
|
|
167
178
|
);
|
|
168
179
|
assert!(
|
|
169
180
|
!raw_path.exists(),
|
|
170
|
-
"
|
|
181
|
+
"F0-2: canonical write_lease_dual_state must not create the raw legacy snapshot: {}",
|
|
171
182
|
raw_path.display()
|
|
172
183
|
);
|
|
173
184
|
|
|
185
|
+
// Diagnostic write path with a divergent pane so we can exercise
|
|
186
|
+
// the divergence reader below.
|
|
174
187
|
let restart_state = serde_json::json!({
|
|
175
188
|
"session_name": "team:proj-中文",
|
|
176
189
|
"team_owner": {"pane_id": "%9", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
|
|
@@ -180,13 +193,18 @@ use super::*;
|
|
|
180
193
|
assert_eq!(restart_path, safe_path);
|
|
181
194
|
assert!(
|
|
182
195
|
restart_path.exists(),
|
|
183
|
-
"
|
|
196
|
+
"diagnostic snapshot must be written to a real file: {}",
|
|
184
197
|
restart_path.display()
|
|
185
198
|
);
|
|
186
199
|
|
|
200
|
+
// F0-2 RED1 diagnostic marker is present on the on-disk file.
|
|
201
|
+
let snap_state: serde_json::Value =
|
|
202
|
+
serde_json::from_str(&std::fs::read_to_string(&restart_path).unwrap()).unwrap();
|
|
203
|
+
assert_eq!(snap_state["_not_authoritative"], serde_json::json!(true));
|
|
204
|
+
|
|
187
205
|
let d = detect_dual_state_divergence(&ws, &state)
|
|
188
206
|
.unwrap()
|
|
189
|
-
.expect("divergence detector must read the
|
|
207
|
+
.expect("divergence detector must read the diagnostic snapshot");
|
|
190
208
|
assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
|
|
191
209
|
assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
|
|
192
210
|
assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
|
|
@@ -10,13 +10,16 @@ use super::*;
|
|
|
10
10
|
/// (bound-pane liveness + caller eligibility) deterministically with no tmux.
|
|
11
11
|
#[test]
|
|
12
12
|
#[serial_test::serial(env)]
|
|
13
|
-
|
|
13
|
+
// Foundation-0 F0-2 conversion: `claim_lease_no_incident` used to
|
|
14
|
+
// dual-write the legacy per-session snapshot; that is retired
|
|
15
|
+
// (`.team/artifacts/foundation-0-slice-design.md` §§4-5). Canonical
|
|
16
|
+
// teams.<team_key> now carries the sole authoritative owner tuple.
|
|
17
|
+
fn claim_lease_no_incident_vacant_acquire_advances_epoch_and_writes_canonical_only() {
|
|
14
18
|
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
15
19
|
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
16
20
|
let ws = p2_temp_ws("claim_vacant");
|
|
17
21
|
let team_id = TeamKey::new("current");
|
|
18
22
|
let caller = PaneId::new("%5");
|
|
19
|
-
// empty (vacant) state with a session so dual-state writes both files.
|
|
20
23
|
let mut state = serde_json::json!({"session_name": "team-agent-x"});
|
|
21
24
|
let event_log = crate::event_log::EventLog::new(&ws);
|
|
22
25
|
let live = seeded_liveness(&["%5"]);
|
|
@@ -35,45 +38,25 @@ use super::*;
|
|
|
35
38
|
let receiver = r.receiver.as_ref().expect("claimed → receiver");
|
|
36
39
|
assert_eq!(receiver.pane_id, caller);
|
|
37
40
|
assert_eq!(receiver.discovery, Some(Discovery::ClaimLeader));
|
|
38
|
-
// Stage 3d (identity-boundary unified plan, architect direction
|
|
39
|
-
// 2026-06-23): canonical owner now lives at teams.<team_key>; the
|
|
40
|
-
// top-level dual-write was dropped. Read through the ownership
|
|
41
|
-
// repository which preserves the legacy precedence for callers
|
|
42
|
-
// that still consult top-level on migration states.
|
|
43
41
|
let ws_path = crate::state::persist::runtime_state_path(&ws);
|
|
44
42
|
let ws_state: serde_json::Value =
|
|
45
43
|
serde_json::from_str(&std::fs::read_to_string(&ws_path).unwrap()).unwrap();
|
|
46
|
-
// The canonical team_key is what `team_state_key(state)` derives
|
|
47
|
-
// (here: state's session_name "team-agent-x" since no explicit
|
|
48
|
-
// team_key/team_dir/spec_path was seeded). The test's `team_id`
|
|
49
|
-
// parameter is the LOOKUP key but `claim_lease_no_incident` uses
|
|
50
|
-
// the state-derived key for canonical writes. Stage 5 will unify
|
|
51
|
-
// these via TeamRuntimePaths.
|
|
52
44
|
let canonical_key = crate::state::projection::team_state_key(&ws_state);
|
|
53
45
|
let ws_owner =
|
|
54
46
|
crate::state::ownership::read_owner_value(&ws_state, &canonical_key)
|
|
55
47
|
.expect("Stage 3d: vacant acquire writes canonical owner");
|
|
56
48
|
assert_eq!(ws_owner["pane_id"], serde_json::json!("%5"));
|
|
57
49
|
assert_eq!(ws_owner["owner_epoch"], serde_json::json!(1));
|
|
58
|
-
// team_id parameter is currently unused by the canonical write
|
|
59
|
-
// (architect Stage 5 will close this gap); silence the warning.
|
|
60
50
|
let _ = &team_id;
|
|
51
|
+
// F0-2: canonical claim path must NOT side-effect the legacy
|
|
52
|
+
// per-session snapshot any more.
|
|
61
53
|
let snap_path = crate::model::paths::runtime_dir(&ws)
|
|
62
54
|
.join("teams").join("team-agent-x").join("state.json");
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
let snap_owner = snap
|
|
69
|
-
.get("team_owner")
|
|
70
|
-
.or_else(|| {
|
|
71
|
-
let key = crate::state::projection::team_state_key(&snap);
|
|
72
|
-
snap.get("teams").and_then(|t| t.get(key)).and_then(|e| e.get("team_owner"))
|
|
73
|
-
})
|
|
74
|
-
.expect("dual-state snapshot owner");
|
|
75
|
-
assert_eq!(snap_owner["pane_id"], serde_json::json!("%5"), "dual-state snapshot owner");
|
|
76
|
-
assert_eq!(snap_owner["owner_epoch"], serde_json::json!(1));
|
|
55
|
+
assert!(
|
|
56
|
+
!snap_path.exists(),
|
|
57
|
+
"F0-2: vacant claim must not write a legacy per-session snapshot; found {}",
|
|
58
|
+
snap_path.display(),
|
|
59
|
+
);
|
|
77
60
|
}
|
|
78
61
|
|
|
79
62
|
// RED — DEAD-OWNER RECOVER: bound pane %1 absent from live set, caller %5
|
|
@@ -6,8 +6,24 @@ use std::path::{Path, PathBuf};
|
|
|
6
6
|
|
|
7
7
|
use super::*;
|
|
8
8
|
|
|
9
|
-
/// `save_team_runtime_snapshot(workspace, state)`
|
|
10
|
-
///
|
|
9
|
+
/// `save_team_runtime_snapshot(workspace, state)` — Foundation-0 F0-2:
|
|
10
|
+
/// this is now a **diagnostic-only** legacy per-session snapshot writer.
|
|
11
|
+
/// Root/projection `.team/runtime/state.json` is the sole product
|
|
12
|
+
/// authority; the file this writes carries `_not_authoritative:true`
|
|
13
|
+
/// and pointers back to the canonical path so no reader can mistake it
|
|
14
|
+
/// for the real runtime state. See
|
|
15
|
+
/// `.team/artifacts/foundation-0-slice-design.md` §§4-5 F0-2.
|
|
16
|
+
///
|
|
17
|
+
/// Product save paths (attach/claim/start-agent/restart/stop/remove)
|
|
18
|
+
/// must NOT call this — the RED2 grep guard enforces that. Retained
|
|
19
|
+
/// callers are diagnostic/migration/test paths only.
|
|
20
|
+
///
|
|
21
|
+
/// Preserves the legacy path shape (`.team/runtime/teams/<session>/state.json`)
|
|
22
|
+
/// so 0.5.x operators inspecting the file see the diagnostic marker
|
|
23
|
+
/// rather than being surprised by a missing file. The B3 canonical
|
|
24
|
+
/// team-key layout will live under a different path
|
|
25
|
+
/// (`teams/<team_key>/...`), so this file also cannot be mistaken for
|
|
26
|
+
/// the future canonical shape.
|
|
11
27
|
pub fn save_team_runtime_snapshot(
|
|
12
28
|
workspace: &Path,
|
|
13
29
|
state: &serde_json::Value,
|
|
@@ -16,17 +32,34 @@ pub fn save_team_runtime_snapshot(
|
|
|
16
32
|
.get("session_name")
|
|
17
33
|
.and_then(|v| v.as_str())
|
|
18
34
|
.ok_or_else(|| LifecycleError::StatePersist("session_name is required".to_string()))?;
|
|
19
|
-
// golden restart/snapshot.py:46-47 — team_runtime_snapshot_dir = runtime_dir(workspace)/teams/<safe>,
|
|
20
|
-
// and paths.py:25-26 runtime_dir = workspace/.team/runtime. Use the crate path helper so the snapshot
|
|
21
|
-
// lands at <ws>/.team/runtime/teams/<safe>, matching golden AND the rest of the crate (not the
|
|
22
|
-
// ".team"-less <ws>/runtime/teams that the original port wrote).
|
|
23
35
|
let path = team_snapshot_path(workspace, session_name);
|
|
24
36
|
let dir = path
|
|
25
37
|
.parent()
|
|
26
38
|
.ok_or_else(|| LifecycleError::StatePersist("snapshot path has no parent".to_string()))?;
|
|
27
39
|
fs::create_dir_all(&dir).map_err(|e| persist_err("create snapshot dir", &e))?;
|
|
40
|
+
let canonical_path = crate::state::persist::runtime_state_path(workspace);
|
|
41
|
+
let generated_at = chrono::Utc::now().to_rfc3339();
|
|
42
|
+
// F0-2 RED1: annotate the snapshot payload with diagnostic-only
|
|
43
|
+
// metadata so any reader can immediately see it is derived, not
|
|
44
|
+
// authoritative. `_canonical_state_path` points at the real
|
|
45
|
+
// runtime authority; `_derived_from` names the code path that
|
|
46
|
+
// produced this file; `_generated_at` records freshness so stale
|
|
47
|
+
// snapshots are easy to identify.
|
|
48
|
+
let mut annotated = state.clone();
|
|
49
|
+
if let Some(obj) = annotated.as_object_mut() {
|
|
50
|
+
obj.insert("_not_authoritative".to_string(), serde_json::json!(true));
|
|
51
|
+
obj.insert(
|
|
52
|
+
"_canonical_state_path".to_string(),
|
|
53
|
+
serde_json::json!(canonical_path.to_string_lossy()),
|
|
54
|
+
);
|
|
55
|
+
obj.insert(
|
|
56
|
+
"_derived_from".to_string(),
|
|
57
|
+
serde_json::json!("lifecycle::save_team_runtime_snapshot"),
|
|
58
|
+
);
|
|
59
|
+
obj.insert("_generated_at".to_string(), serde_json::json!(generated_at));
|
|
60
|
+
}
|
|
28
61
|
let tmp = dir.join("state.json.tmp");
|
|
29
|
-
let data = serde_json::to_vec_pretty(
|
|
62
|
+
let data = serde_json::to_vec_pretty(&annotated)
|
|
30
63
|
.map_err(|e| LifecycleError::StatePersist(format!("serialize snapshot: {e}")))?;
|
|
31
64
|
fs::write(&tmp, data).map_err(|e| persist_err("write snapshot temp", &e))?;
|
|
32
65
|
fs::rename(&tmp, &path).map_err(|e| persist_err("replace snapshot", &e))?;
|
|
@@ -915,12 +915,9 @@ pub(super) fn stop_agent_at_paths(
|
|
|
915
915
|
)
|
|
916
916
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
917
917
|
// golden operations.py:96-99: snapshot (side-effect), then state_file = write_team_state path.
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
if session_name_present(&state) {
|
|
922
|
-
crate::lifecycle::helpers::save_team_runtime_snapshot(workspace, &state)?;
|
|
923
|
-
}
|
|
918
|
+
// Foundation-0 F0-2: legacy per-session snapshot dual-write retired
|
|
919
|
+
// (`.team/artifacts/foundation-0-slice-design.md` §§4-5). Root state
|
|
920
|
+
// remains the only save target on the stop path.
|
|
924
921
|
let state_file = write_team_state(spec_workspace, &spec, &state)?;
|
|
925
922
|
write_stop_complete_event(workspace, agent_id, &target_str, stopped)?;
|
|
926
923
|
Ok(StopAgentReport {
|
|
@@ -186,17 +186,12 @@ fn remove_agent_at_paths(
|
|
|
186
186
|
);
|
|
187
187
|
match result {
|
|
188
188
|
Ok(success) => {
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
.and_then(|v| v.as_str())
|
|
196
|
-
.is_some_and(|s| !s.is_empty())
|
|
197
|
-
{
|
|
198
|
-
let _ = crate::lifecycle::helpers::save_team_runtime_snapshot(workspace, &success.removed_state)?;
|
|
199
|
-
}
|
|
189
|
+
// Foundation-0 F0-2: the historical dual-write to the legacy
|
|
190
|
+
// per-session snapshot has been retired
|
|
191
|
+
// (`.team/artifacts/foundation-0-slice-design.md` §§4-5).
|
|
192
|
+
// Root/projection is the sole runtime authority; the
|
|
193
|
+
// snapshot writer stayed in `lifecycle::helpers` only for
|
|
194
|
+
// diagnostic/migration/test callers.
|
|
200
195
|
write_remove_complete_event(
|
|
201
196
|
paths.run_workspace,
|
|
202
197
|
agent_id,
|
|
@@ -280,8 +280,10 @@ pub(crate) fn latest_reportable_message_for(
|
|
|
280
280
|
agent_id: &str,
|
|
281
281
|
owner_team_id: Option<&str>,
|
|
282
282
|
) -> Option<String> {
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
match direct_message_attribution_for(workspace, agent_id, owner_team_id) {
|
|
284
|
+
DirectMessageAttribution::Reportable(message_id) => Some(message_id),
|
|
285
|
+
DirectMessageAttribution::BlockedByNewer { .. } | DirectMessageAttribution::None => None,
|
|
286
|
+
}
|
|
285
287
|
}
|
|
286
288
|
|
|
287
289
|
pub(crate) fn current_reportable_message_for(
|
|
@@ -306,6 +308,49 @@ pub(crate) fn latest_delivered_direct_message_for(
|
|
|
306
308
|
latest_reportable_message_from_db(&conn, agent_id, owner_team_id)
|
|
307
309
|
}
|
|
308
310
|
|
|
311
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
312
|
+
pub(crate) enum DirectMessageAttribution {
|
|
313
|
+
Reportable(String),
|
|
314
|
+
BlockedByNewer {
|
|
315
|
+
message_id: String,
|
|
316
|
+
status: String,
|
|
317
|
+
error: Option<String>,
|
|
318
|
+
},
|
|
319
|
+
None,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
pub(crate) fn direct_message_attribution_for(
|
|
323
|
+
workspace: &Path,
|
|
324
|
+
agent_id: &str,
|
|
325
|
+
owner_team_id: Option<&str>,
|
|
326
|
+
) -> DirectMessageAttribution {
|
|
327
|
+
use crate::db::message_store::MessageStore;
|
|
328
|
+
let Ok(store) = MessageStore::open(workspace) else {
|
|
329
|
+
return DirectMessageAttribution::None;
|
|
330
|
+
};
|
|
331
|
+
let Ok(conn) = crate::db::schema::open_db(store.db_path()) else {
|
|
332
|
+
return DirectMessageAttribution::None;
|
|
333
|
+
};
|
|
334
|
+
if let Some(message_id) = current_turn_message_for(workspace, &conn, agent_id, owner_team_id) {
|
|
335
|
+
return DirectMessageAttribution::Reportable(message_id);
|
|
336
|
+
}
|
|
337
|
+
match latest_direct_message_from_db(&conn, agent_id, owner_team_id) {
|
|
338
|
+
Some(candidate)
|
|
339
|
+
if reportable_message_status(&candidate.status, candidate.error.as_deref()) =>
|
|
340
|
+
{
|
|
341
|
+
DirectMessageAttribution::Reportable(candidate.message_id)
|
|
342
|
+
}
|
|
343
|
+
Some(candidate) if blocks_historical_fallback(&candidate.status) => {
|
|
344
|
+
DirectMessageAttribution::BlockedByNewer {
|
|
345
|
+
message_id: candidate.message_id,
|
|
346
|
+
status: candidate.status,
|
|
347
|
+
error: candidate.error,
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
Some(_) | None => DirectMessageAttribution::None,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
309
354
|
fn current_turn_message_for(
|
|
310
355
|
workspace: &Path,
|
|
311
356
|
conn: &rusqlite::Connection,
|
|
@@ -419,9 +464,27 @@ fn latest_reportable_message_from_db(
|
|
|
419
464
|
agent_id: &str,
|
|
420
465
|
owner_team_id: Option<&str>,
|
|
421
466
|
) -> Option<String> {
|
|
422
|
-
let
|
|
423
|
-
|
|
424
|
-
|
|
467
|
+
let candidate = latest_direct_message_from_db(conn, agent_id, owner_team_id)?;
|
|
468
|
+
if reportable_message_status(&candidate.status, candidate.error.as_deref()) {
|
|
469
|
+
Some(candidate.message_id)
|
|
470
|
+
} else {
|
|
471
|
+
None
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
struct DirectMessageCandidate {
|
|
476
|
+
message_id: String,
|
|
477
|
+
status: String,
|
|
478
|
+
error: Option<String>,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fn latest_direct_message_from_db(
|
|
482
|
+
conn: &rusqlite::Connection,
|
|
483
|
+
agent_id: &str,
|
|
484
|
+
owner_team_id: Option<&str>,
|
|
485
|
+
) -> Option<DirectMessageCandidate> {
|
|
486
|
+
conn.query_row(
|
|
487
|
+
"select m.message_id, m.status, m.error from messages m
|
|
425
488
|
where m.recipient = ?1
|
|
426
489
|
and (?2 is null or m.owner_team_id = ?2)
|
|
427
490
|
and (m.task_id is null or m.task_id = '')
|
|
@@ -437,29 +500,25 @@ fn latest_reportable_message_from_db(
|
|
|
437
500
|
order by m.created_at desc,
|
|
438
501
|
case when m.status = 'delivered' then 0 else 1 end
|
|
439
502
|
limit 1",
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
let (message_id, status, error) = row;
|
|
453
|
-
if reportable_message_status(&status, error.as_deref()) {
|
|
454
|
-
Some(message_id)
|
|
455
|
-
} else {
|
|
456
|
-
None
|
|
457
|
-
}
|
|
503
|
+
params![agent_id, owner_team_id],
|
|
504
|
+
|row| {
|
|
505
|
+
Ok(DirectMessageCandidate {
|
|
506
|
+
message_id: row.get::<_, String>(0)?,
|
|
507
|
+
status: row.get::<_, String>(1)?,
|
|
508
|
+
error: row.get::<_, Option<String>>(2)?,
|
|
509
|
+
})
|
|
510
|
+
},
|
|
511
|
+
)
|
|
512
|
+
.optional()
|
|
513
|
+
.ok()
|
|
514
|
+
.flatten()
|
|
458
515
|
}
|
|
459
516
|
|
|
460
517
|
fn reportable_message_status(status: &str, error: Option<&str>) -> bool {
|
|
461
|
-
matches!(
|
|
462
|
-
status
|
|
463
|
-
|
|
464
|
-
|
|
518
|
+
matches!(status, "delivered" | "submitted" | "injected" | "visible")
|
|
519
|
+
&& (status == "delivered" || error.is_none())
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fn blocks_historical_fallback(status: &str) -> bool {
|
|
523
|
+
matches!(status, "failed" | "refused" | "blocked")
|
|
465
524
|
}
|
|
@@ -50,6 +50,7 @@ pub(crate) fn reset_agent(
|
|
|
50
50
|
})),
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
emit_newer_daemon_preserved_if_present(workspace)?;
|
|
53
54
|
let lifecycle_workspace = lifecycle_workspace(workspace, owner_team, true)?;
|
|
54
55
|
match crate::lifecycle::reset_agent(
|
|
55
56
|
&lifecycle_workspace,
|
|
@@ -97,6 +98,7 @@ pub(crate) fn fork_agent(
|
|
|
97
98
|
as_agent_id: &str,
|
|
98
99
|
label: Option<&str>,
|
|
99
100
|
) -> ToolResult {
|
|
101
|
+
emit_newer_daemon_preserved_if_present(workspace)?;
|
|
100
102
|
let lifecycle_workspace = lifecycle_workspace(workspace, owner_team, false)?;
|
|
101
103
|
// operations.py:315 — the label becomes the forked agent's role.
|
|
102
104
|
let report = crate::lifecycle::launch::fork_agent(
|
|
@@ -128,6 +130,41 @@ fn reset_refusal_reason(reason: ResetRefusal) -> Value {
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
fn emit_newer_daemon_preserved_if_present(workspace: &Path) -> Result<(), super::super::ToolError> {
|
|
134
|
+
let workspace_path = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
135
|
+
let health = crate::coordinator::coordinator_health(&workspace_path);
|
|
136
|
+
if !(health.service_available
|
|
137
|
+
&& !health.binary_identity_ok
|
|
138
|
+
&& matches!(
|
|
139
|
+
health.binary_identity_relation,
|
|
140
|
+
crate::coordinator::CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
|
|
141
|
+
))
|
|
142
|
+
{
|
|
143
|
+
return Ok(());
|
|
144
|
+
}
|
|
145
|
+
crate::event_log::EventLog::new(workspace)
|
|
146
|
+
.write(
|
|
147
|
+
"coordinator.newer_daemon_preserved",
|
|
148
|
+
serde_json::json!({
|
|
149
|
+
"pid": health.pid.map(|pid| pid.get()),
|
|
150
|
+
"binary_identity_relation": health.binary_identity_relation.as_str(),
|
|
151
|
+
"reason": "daemon_newer_than_caller",
|
|
152
|
+
"daemon_binary_path": health
|
|
153
|
+
.metadata
|
|
154
|
+
.as_ref()
|
|
155
|
+
.and_then(|metadata| metadata.binary_path.clone()),
|
|
156
|
+
"daemon_binary_version": health
|
|
157
|
+
.metadata
|
|
158
|
+
.as_ref()
|
|
159
|
+
.and_then(|metadata| metadata.binary_version.clone()),
|
|
160
|
+
"caller_binary_path": health.current_binary_identity.binary_path,
|
|
161
|
+
"caller_binary_version": health.current_binary_identity.binary_version,
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
.map(|_| ())
|
|
165
|
+
.map_err(tool_runtime_error)
|
|
166
|
+
}
|
|
167
|
+
|
|
131
168
|
fn lifecycle_workspace(
|
|
132
169
|
workspace: &Path,
|
|
133
170
|
owner_team: Option<&TeamKey>,
|
|
@@ -20,10 +20,10 @@ use crate::state::persist::{
|
|
|
20
20
|
use crate::messaging::{self, MessageTarget, SendOptions};
|
|
21
21
|
|
|
22
22
|
use super::helpers::{
|
|
23
|
-
|
|
24
|
-
insert_array, is_worker_recipient, json_dumps_default,
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
delivery_outcome_value, direct_message_attribution_for, ensure_object, enum_value,
|
|
24
|
+
insert_array, is_worker_recipient, json_dumps_default, latest_task_for_assignee,
|
|
25
|
+
non_empty_string, normalized_envelope_value, object_fields, requires_ack_for_target,
|
|
26
|
+
tool_runtime_error, DirectMessageAttribution,
|
|
27
27
|
};
|
|
28
28
|
use super::normalize::{
|
|
29
29
|
compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
|
|
@@ -336,41 +336,49 @@ impl TeamOrchestratorTools {
|
|
|
336
336
|
// message-scoped fallback, before defaulting to "manual".
|
|
337
337
|
// 1. explicit arg
|
|
338
338
|
// 2. current physical direct turn for this agent
|
|
339
|
-
// 3. latest
|
|
340
|
-
// 4. latest nonterminal assigned task in teams.<owner>.tasks
|
|
341
|
-
//
|
|
339
|
+
// 3. bounded latest reportable direct message with no result yet
|
|
340
|
+
// 4. latest nonterminal assigned task in teams.<owner>.tasks,
|
|
341
|
+
// only when no newer failed/refused/blocked direct turn blocks fallback
|
|
342
342
|
// 5. "manual" — truly uncorrelated; collect still rejects
|
|
343
343
|
let owner_team_id_str = self.owner_team_id.as_ref().map(|t| t.as_str().to_string());
|
|
344
|
-
let resolved = task_id
|
|
345
|
-
.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
344
|
+
let resolved = if let Some(task_id) = task_id {
|
|
345
|
+
task_id.to_string()
|
|
346
|
+
} else if let Some(agent) = self.agent_id.as_ref() {
|
|
347
|
+
match direct_message_attribution_for(
|
|
348
|
+
&self.workspace,
|
|
349
|
+
agent.as_str(),
|
|
350
|
+
owner_team_id_str.as_deref(),
|
|
351
|
+
) {
|
|
352
|
+
DirectMessageAttribution::Reportable(message_id) => message_id,
|
|
353
|
+
DirectMessageAttribution::BlockedByNewer {
|
|
354
|
+
message_id,
|
|
355
|
+
status,
|
|
356
|
+
error,
|
|
357
|
+
} => {
|
|
358
|
+
push_report_warning(
|
|
359
|
+
obj,
|
|
360
|
+
serde_json::json!({
|
|
361
|
+
"code": "result_attribution_blocked_by_newer_direct_message",
|
|
362
|
+
"field": "task_id",
|
|
363
|
+
"severity": "warning",
|
|
364
|
+
"advisory": true,
|
|
365
|
+
"message_id": message_id,
|
|
366
|
+
"message_status": status,
|
|
367
|
+
"message_error": error,
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
"manual".to_string()
|
|
371
|
+
}
|
|
372
|
+
DirectMessageAttribution::None => latest_task_for_assignee(
|
|
373
|
+
&self.workspace,
|
|
374
|
+
agent.as_str(),
|
|
375
|
+
owner_team_id_str.as_deref(),
|
|
376
|
+
)
|
|
377
|
+
.unwrap_or_else(|| "manual".to_string()),
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
"manual".to_string()
|
|
381
|
+
};
|
|
374
382
|
obj.insert("task_id".to_string(), Value::String(resolved));
|
|
375
383
|
}
|
|
376
384
|
if !obj.contains_key("agent_id") {
|
|
@@ -977,6 +985,29 @@ fn merge_object_fields(existing: &mut Value, incoming: &Value) {
|
|
|
977
985
|
}
|
|
978
986
|
}
|
|
979
987
|
|
|
988
|
+
fn push_report_warning(obj: &mut serde_json::Map<String, Value>, warning: Value) {
|
|
989
|
+
let Some(code) = warning.get("code").and_then(Value::as_str) else {
|
|
990
|
+
return;
|
|
991
|
+
};
|
|
992
|
+
let Some(field) = warning.get("field").and_then(Value::as_str) else {
|
|
993
|
+
return;
|
|
994
|
+
};
|
|
995
|
+
match obj.get_mut("warnings") {
|
|
996
|
+
Some(Value::Array(warnings)) => {
|
|
997
|
+
let exists = warnings.iter().any(|existing| {
|
|
998
|
+
existing.get("code").and_then(Value::as_str) == Some(code)
|
|
999
|
+
&& existing.get("field").and_then(Value::as_str) == Some(field)
|
|
1000
|
+
});
|
|
1001
|
+
if !exists {
|
|
1002
|
+
warnings.push(warning);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
_ => {
|
|
1006
|
+
obj.insert("warnings".to_string(), Value::Array(vec![warning]));
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
980
1011
|
fn write_team_tasks(state: &mut Value, team_key: &str, tasks: Vec<Value>) {
|
|
981
1012
|
let Some(root) = state.as_object_mut() else {
|
|
982
1013
|
return;
|
|
@@ -319,6 +319,30 @@ fn coordinator_unavailable_outcome(
|
|
|
319
319
|
if health.ok || matches!(health.status, CoordinatorHealthStatus::Missing) {
|
|
320
320
|
return Ok(None);
|
|
321
321
|
}
|
|
322
|
+
if health.service_available {
|
|
323
|
+
event_log.write(
|
|
324
|
+
"send.coordinator_binary_identity_drift_ignored",
|
|
325
|
+
serde_json::json!({
|
|
326
|
+
"recipient": recipient,
|
|
327
|
+
"sender": opts.sender,
|
|
328
|
+
"coordinator_status": health.status,
|
|
329
|
+
"coordinator_pid": health.pid.map(|pid| pid.get()),
|
|
330
|
+
"message_queued": true,
|
|
331
|
+
"binary_identity_relation": health.binary_identity_relation.as_str(),
|
|
332
|
+
"daemon_binary_path": health
|
|
333
|
+
.metadata
|
|
334
|
+
.as_ref()
|
|
335
|
+
.and_then(|metadata| metadata.binary_path.clone()),
|
|
336
|
+
"daemon_binary_version": health
|
|
337
|
+
.metadata
|
|
338
|
+
.as_ref()
|
|
339
|
+
.and_then(|metadata| metadata.binary_version.clone()),
|
|
340
|
+
"caller_binary_path": health.current_binary_identity.binary_path,
|
|
341
|
+
"caller_binary_version": health.current_binary_identity.binary_version,
|
|
342
|
+
}),
|
|
343
|
+
)?;
|
|
344
|
+
return Ok(None);
|
|
345
|
+
}
|
|
322
346
|
let warning = format!(
|
|
323
347
|
"coordinator is not running; message was not queued for {recipient}. Run `team-agent diagnose` or restart the team before sending again."
|
|
324
348
|
);
|
|
@@ -329,6 +353,7 @@ fn coordinator_unavailable_outcome(
|
|
|
329
353
|
"sender": opts.sender,
|
|
330
354
|
"coordinator_status": health.status,
|
|
331
355
|
"coordinator_pid": health.pid.map(|pid| pid.get()),
|
|
356
|
+
"metadata_mismatch_reason": health.metadata_mismatch_reason,
|
|
332
357
|
"message_queued": false,
|
|
333
358
|
"warning": warning,
|
|
334
359
|
"coordinator_log": crate::coordinator::coordinator_log_path(&coordinator_workspace)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.24",
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.24",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.24",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.24"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|