@team-agent/installer 0.5.24 → 0.5.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.24"
578
+ version = "0.5.25"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.24"
12
+ version = "0.5.25"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -171,10 +171,23 @@ pub(crate) fn diagnose_runtime_for_workspace(
171
171
  backend: &dyn Transport,
172
172
  ) -> (Value, Value) {
173
173
  let (mut issues, mut repairs) = diagnose_runtime(state, backend);
174
+ append_legacy_snapshot_issue(workspace, state, &mut issues);
174
175
  append_coordinator_health_issue(workspace, state, &mut issues, &mut repairs);
175
176
  (issues, repairs)
176
177
  }
177
178
 
179
+ fn append_legacy_snapshot_issue(workspace: &std::path::Path, state: &Value, issues: &mut Value) {
180
+ let Ok(Some(details)) = crate::leader::detect_dual_state_divergence(workspace, state) else {
181
+ return;
182
+ };
183
+ if let Some(items) = issues.as_array_mut() {
184
+ items.push(json!({
185
+ "id": "legacy_snapshot_stale",
186
+ "details": details,
187
+ }));
188
+ }
189
+ }
190
+
178
191
  fn append_coordinator_health_issue(
179
192
  workspace: &std::path::Path,
180
193
  state: &Value,
@@ -1780,10 +1780,13 @@ pub fn detect_dual_state_divergence( // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: diag
1780
1780
  .or_else(|| get_path_u64(state, &["leader_receiver", "owner_epoch"]));
1781
1781
  let team_epoch = get_path_u64(&snap, &["team_owner", "owner_epoch"])
1782
1782
  .or_else(|| get_path_u64(&snap, &["leader_receiver", "owner_epoch"]));
1783
+ let workspace_agent_bindings = agent_binding_summary(state);
1784
+ let team_agent_bindings = agent_binding_summary(&snap);
1783
1785
  let diverged = workspace_owner_pane != team_owner_pane
1784
1786
  || workspace_owner_uuid != team_owner_uuid
1785
1787
  || workspace_receiver_pane != team_receiver_pane
1786
- || workspace_epoch != team_epoch;
1788
+ || workspace_epoch != team_epoch
1789
+ || workspace_agent_bindings != team_agent_bindings;
1787
1790
  if !diverged {
1788
1791
  return Ok(None);
1789
1792
  }
@@ -1796,10 +1799,38 @@ pub fn detect_dual_state_divergence( // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: diag
1796
1799
  "team_receiver_pane": team_receiver_pane,
1797
1800
  "workspace_owner_epoch": workspace_epoch,
1798
1801
  "team_owner_epoch": team_epoch,
1802
+ "workspace_agent_bindings": workspace_agent_bindings,
1803
+ "team_agent_bindings": team_agent_bindings,
1799
1804
  "_legacy_snapshot_stale": true,
1800
1805
  })))
1801
1806
  }
1802
1807
 
1808
+ fn agent_binding_summary(state: &Value) -> Value {
1809
+ let mut out = serde_json::Map::new();
1810
+ let Some(agents) = state.get("agents").and_then(Value::as_object) else {
1811
+ return Value::Object(out);
1812
+ };
1813
+ for (agent_id, agent) in agents {
1814
+ let mut binding = serde_json::Map::new();
1815
+ for key in [
1816
+ "pane_id",
1817
+ "pane_pid",
1818
+ "tmux_endpoint",
1819
+ "tmux_socket",
1820
+ "window",
1821
+ "window_name",
1822
+ ] {
1823
+ if let Some(value) = agent.get(key) {
1824
+ binding.insert(key.to_string(), value.clone());
1825
+ }
1826
+ }
1827
+ if !binding.is_empty() {
1828
+ out.insert(agent_id.clone(), Value::Object(binding));
1829
+ }
1830
+ }
1831
+ Value::Object(out)
1832
+ }
1833
+
1803
1834
  fn readable_team_snapshot_path(workspace: &Path, session_name: &str) -> PathBuf { // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: diagnostic-only path resolver.
1804
1835
  let safe_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name); // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: reuses helpers safe legacy path.
1805
1836
  if safe_path.exists() {
@@ -238,6 +238,9 @@ pub fn compact_tool_result(result: &Value) -> ToolResult {
238
238
  "durably_stored",
239
239
  "result_id",
240
240
  "task_id",
241
+ "attributed_message_id",
242
+ "attribution_scope",
243
+ "task_id_source",
241
244
  "agent_id",
242
245
  "new_agent_id",
243
246
  "source_agent_id",
@@ -20,10 +20,10 @@ use crate::state::persist::{
20
20
  use crate::messaging::{self, MessageTarget, SendOptions};
21
21
 
22
22
  use super::helpers::{
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,
23
+ current_reportable_message_for, delivery_outcome_value, direct_message_attribution_for,
24
+ ensure_object, enum_value, insert_array, is_worker_recipient, json_dumps_default,
25
+ latest_task_for_assignee, non_empty_string, normalized_envelope_value, object_fields,
26
+ requires_ack_for_target, tool_runtime_error, DirectMessageAttribution,
27
27
  };
28
28
  use super::normalize::{
29
29
  compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
@@ -341,45 +341,76 @@ impl TeamOrchestratorTools {
341
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 mut attributed_message_id = None;
345
+ let mut attribution_scope = None;
346
+ let mut task_id_source = None;
344
347
  let resolved = if let Some(task_id) = task_id {
345
348
  task_id.to_string()
346
349
  } else if let Some(agent) = self.agent_id.as_ref() {
347
- match direct_message_attribution_for(
350
+ if let Some(message_id) = current_reportable_message_for(
348
351
  &self.workspace,
349
352
  agent.as_str(),
350
353
  owner_team_id_str.as_deref(),
351
354
  ) {
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(
355
+ attributed_message_id = Some(message_id.clone());
356
+ attribution_scope = Some("message");
357
+ task_id_source = Some("current_turn_message");
358
+ message_id
359
+ } else {
360
+ match direct_message_attribution_for(
373
361
  &self.workspace,
374
362
  agent.as_str(),
375
363
  owner_team_id_str.as_deref(),
376
- )
377
- .unwrap_or_else(|| "manual".to_string()),
364
+ ) {
365
+ DirectMessageAttribution::Reportable(message_id) => {
366
+ attributed_message_id = Some(message_id.clone());
367
+ attribution_scope = Some("message");
368
+ task_id_source = Some("direct_message");
369
+ message_id
370
+ }
371
+ DirectMessageAttribution::BlockedByNewer {
372
+ message_id,
373
+ status,
374
+ error,
375
+ } => {
376
+ push_report_warning(
377
+ obj,
378
+ serde_json::json!({
379
+ "code": "result_attribution_blocked_by_newer_direct_message",
380
+ "field": "task_id",
381
+ "severity": "warning",
382
+ "advisory": true,
383
+ "message_id": message_id,
384
+ "message_status": status,
385
+ "message_error": error,
386
+ }),
387
+ );
388
+ "manual".to_string()
389
+ }
390
+ DirectMessageAttribution::None => latest_task_for_assignee(
391
+ &self.workspace,
392
+ agent.as_str(),
393
+ owner_team_id_str.as_deref(),
394
+ )
395
+ .unwrap_or_else(|| "manual".to_string()),
396
+ }
378
397
  }
379
398
  } else {
380
399
  "manual".to_string()
381
400
  };
382
401
  obj.insert("task_id".to_string(), Value::String(resolved));
402
+ if let Some(message_id) = attributed_message_id {
403
+ obj.entry("attributed_message_id")
404
+ .or_insert(Value::String(message_id));
405
+ }
406
+ if let Some(scope) = attribution_scope {
407
+ obj.entry("attribution_scope")
408
+ .or_insert(Value::String(scope.to_string()));
409
+ }
410
+ if let Some(source) = task_id_source {
411
+ obj.entry("task_id_source")
412
+ .or_insert(Value::String(source.to_string()));
413
+ }
383
414
  }
384
415
  if !obj.contains_key("agent_id") {
385
416
  let resolved = agent_id
@@ -419,6 +450,7 @@ impl TeamOrchestratorTools {
419
450
  let normalized = normalize_report_envelope(&base);
420
451
  let warnings = report_result_integrity_warnings(&base, &normalized);
421
452
  let mut env_value = normalized_envelope_value(&normalized);
453
+ copy_report_attribution_fields(&base, &mut env_value);
422
454
  if !warnings.is_empty() {
423
455
  if let Some(obj) = env_value.as_object_mut() {
424
456
  obj.insert("warnings".to_string(), Value::Array(warnings));
@@ -985,6 +1017,24 @@ fn merge_object_fields(existing: &mut Value, incoming: &Value) {
985
1017
  }
986
1018
  }
987
1019
 
1020
+ fn copy_report_attribution_fields(source: &Value, target: &mut Value) {
1021
+ let Some(src) = source.as_object() else {
1022
+ return;
1023
+ };
1024
+ let Some(dst) = target.as_object_mut() else {
1025
+ return;
1026
+ };
1027
+ for key in [
1028
+ "attributed_message_id",
1029
+ "attribution_scope",
1030
+ "task_id_source",
1031
+ ] {
1032
+ if let Some(value) = src.get(key) {
1033
+ dst.entry(key.to_string()).or_insert(value.clone());
1034
+ }
1035
+ }
1036
+ }
1037
+
988
1038
  fn push_report_warning(obj: &mut serde_json::Map<String, Value>, warning: Value) {
989
1039
  let Some(code) = warning.get("code").and_then(Value::as_str) else {
990
1040
  return;
@@ -697,6 +697,7 @@ fn report_result_for_owner_team_inner(
697
697
  "task_id".to_string(),
698
698
  serde_json::Value::String(task_id.to_string()),
699
699
  );
700
+ copy_report_attribution_fields(envelope, &mut out);
700
701
  out.insert(
701
702
  "agent_id".to_string(),
702
703
  serde_json::Value::String(agent_id.to_string()),
@@ -927,6 +928,7 @@ fn report_result_for_owner_team_inner(
927
928
  "task_id".to_string(),
928
929
  serde_json::Value::String(task_id.to_string()),
929
930
  );
931
+ copy_report_attribution_fields(envelope, &mut out);
930
932
  out.insert(
931
933
  "agent_id".to_string(),
932
934
  serde_json::Value::String(agent_id.to_string()),
@@ -968,6 +970,21 @@ fn report_result_for_owner_team_inner(
968
970
  Ok(serde_json::Value::Object(out))
969
971
  }
970
972
 
973
+ fn copy_report_attribution_fields(
974
+ envelope: &serde_json::Value,
975
+ out: &mut serde_json::Map<String, serde_json::Value>,
976
+ ) {
977
+ for key in [
978
+ "attributed_message_id",
979
+ "attribution_scope",
980
+ "task_id_source",
981
+ ] {
982
+ if let Some(value) = envelope.get(key) {
983
+ out.insert(key.to_string(), value.clone());
984
+ }
985
+ }
986
+ }
987
+
971
988
  fn fallback_primary_error_text(cli_primary_error: Option<&str>, observed: String) -> String {
972
989
  match cli_primary_error.filter(|error| !error.trim().is_empty()) {
973
990
  Some(error) => format!("{error}; {observed}"),
@@ -5,7 +5,7 @@
5
5
  //! paths or behaviour. It introduces a shared vocabulary that Stage 2 (owner
6
6
  //! repository), Stage 5 (per-team runtime state), and Stage 6 (per-team
7
7
  //! coordinator) will all consume, so that no later stage has to hand-build
8
- //! `.team/runtime/<team_key>/...` paths or guess the canonical team_key from
8
+ //! `.team/runtime/teams/<team_key>/...` paths or guess the canonical team_key from
9
9
  //! a display name.
10
10
  //!
11
11
  //! Canonical rules:
@@ -15,10 +15,10 @@
15
15
  //! - For the foundation slice, `TeamScope::new(team_key)` accepts whatever
16
16
  //! the caller already resolved (state/selector + state/persist already do
17
17
  //! this work). Slug/hash promotion to global uniqueness lands in Stage 5.
18
- //! - `TeamRuntimePaths` is the SINGLE place where the
19
- //! `.team/runtime/<team_key>/` layout is constructed. Anywhere downstream
20
- //! code today hand-joins `runtime_dir(ws).join(team_key).join(...)` it
21
- //! should migrate to call a method on `TeamRuntimePaths` in a later stage.
18
+ //! - `TeamRuntimePaths` is the SINGLE place where the future B3
19
+ //! `.team/runtime/teams/<team_key>/` layout is constructed. Anywhere
20
+ //! downstream code today hand-joins runtime team paths should migrate to
21
+ //! call a method on `TeamRuntimePaths` in a later stage.
22
22
  //!
23
23
  //! Single-team behaviour: unchanged. The foundation does not move data, does
24
24
  //! not introduce new files, and is invoked by zero existing call sites yet.
@@ -72,15 +72,15 @@ impl TeamScope {
72
72
  }
73
73
  }
74
74
 
75
- /// Single source of `.team/runtime/<team_key>/...` path construction.
75
+ /// Single source of future B3 `.team/runtime/teams/<team_key>/...` path construction.
76
76
  ///
77
77
  /// Stage 2 (owner repository), Stage 5 (per-team state), Stage 6 (per-team
78
78
  /// coordinator), and Stage 7 (per-team tmux socket) will all migrate from
79
79
  /// hand-built paths to `TeamRuntimePaths`. Today nothing reads from these
80
80
  /// methods yet — the foundation just owns the layout decision so when later
81
- /// stages start writing `.team/runtime/<team_key>/state.json` (Stage 5) or
82
- /// `.team/runtime/<team_key>/coordinator.pid` (Stage 6), there is exactly
83
- /// one place to change the layout.
81
+ /// stages start writing `.team/runtime/teams/<team_key>/state.json` (Stage 5)
82
+ /// or `.team/runtime/teams/<team_key>/coordinator.pid` (Stage 6), there is
83
+ /// exactly one place to change the layout.
84
84
  #[derive(Debug, Clone)]
85
85
  pub struct TeamRuntimePaths {
86
86
  workspace: PathBuf,
@@ -110,34 +110,36 @@ impl TeamRuntimePaths {
110
110
  &self.team_key
111
111
  }
112
112
 
113
- /// `.team/runtime/<team_key>/` — the team's runtime directory. Today this
114
- /// already exists (runtime_spec lives under it). Stage 5 will start
115
- /// writing `state.json` here too.
113
+ /// `.team/runtime/teams/<team_key>/` — the team's future B3 runtime
114
+ /// directory. It is not used as product authority until the B3 migration;
115
+ /// current callers must keep using `runtime_state_path`.
116
116
  pub fn team_dir(&self) -> PathBuf {
117
- runtime_dir(&self.workspace).join(&self.team_key)
117
+ runtime_dir(&self.workspace)
118
+ .join("teams")
119
+ .join(&self.team_key)
118
120
  }
119
121
 
120
- /// `.team/runtime/<team_key>/team.spec.yaml` runtime spec. Mirrors the
121
- /// existing `runtime_spec_path` helper; provided here so downstream
122
- /// callers don't need to import two layout APIs.
122
+ /// Transitional runtime spec path. Mirrors the existing
123
+ /// `runtime_spec_path` helper while specs remain at
124
+ /// `.team/runtime/<team_key>/team.spec.yaml`.
123
125
  pub fn spec_path(&self) -> PathBuf {
124
126
  runtime_spec_path(&self.workspace, &self.team_key)
125
127
  }
126
128
 
127
- /// `.team/runtime/<team_key>/state.json` — the canonical per-team state
128
- /// path that Stage 5 will start writing. Not used by Stage 0 product
129
- /// code; callers must continue using `runtime_state_path` until Stage 5
129
+ /// `.team/runtime/teams/<team_key>/state.json` — the canonical per-team
130
+ /// state path that B3 will start writing. Not used by current product
131
+ /// code; callers must continue using `runtime_state_path` until B3
130
132
  /// migrates the truth source.
131
133
  pub fn state_path(&self) -> PathBuf {
132
134
  self.team_dir().join("state.json")
133
135
  }
134
136
 
135
- /// `.team/runtime/<team_key>/coordinator.pid` — Stage 6 sidecar location.
137
+ /// `.team/runtime/teams/<team_key>/coordinator.pid` — future sidecar location.
136
138
  pub fn coordinator_pid_path(&self) -> PathBuf {
137
139
  self.team_dir().join("coordinator.pid")
138
140
  }
139
141
 
140
- /// `.team/runtime/<team_key>/coordinator.log` — Stage 6 sidecar location.
142
+ /// `.team/runtime/teams/<team_key>/coordinator.log` — future sidecar location.
141
143
  pub fn coordinator_log_path(&self) -> PathBuf {
142
144
  self.team_dir().join("coordinator.log")
143
145
  }
@@ -240,23 +242,20 @@ mod tests {
240
242
  }
241
243
 
242
244
  #[test]
243
- fn team_runtime_paths_layout_matches_existing_runtime_dir_layout() {
245
+ fn team_runtime_paths_layout_preannounces_b3_state_dir() {
244
246
  let paths = TeamRuntimePaths::new(PathBuf::from("/ws/proj"), "alpha");
245
- assert_eq!(
246
- paths.team_dir(),
247
- PathBuf::from("/ws/proj/.team/runtime/alpha")
248
- );
247
+ let b3_team_dir = PathBuf::from("/ws/proj/.team/runtime")
248
+ .join("teams")
249
+ .join("alpha");
250
+ assert_eq!(paths.team_dir(), b3_team_dir);
249
251
  assert_eq!(
250
252
  paths.spec_path(),
251
253
  PathBuf::from("/ws/proj/.team/runtime/alpha/team.spec.yaml")
252
254
  );
253
- assert_eq!(
254
- paths.state_path(),
255
- PathBuf::from("/ws/proj/.team/runtime/alpha/state.json")
256
- );
255
+ assert_eq!(paths.state_path(), b3_team_dir.join("state.json"));
257
256
  assert_eq!(
258
257
  paths.coordinator_pid_path(),
259
- PathBuf::from("/ws/proj/.team/runtime/alpha/coordinator.pid")
258
+ b3_team_dir.join("coordinator.pid")
260
259
  );
261
260
  }
262
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.24",
3
+ "version": "0.5.25",
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",
24
- "@team-agent/cli-darwin-x64": "0.5.24",
25
- "@team-agent/cli-linux-x64": "0.5.24"
23
+ "@team-agent/cli-darwin-arm64": "0.5.25",
24
+ "@team-agent/cli-darwin-x64": "0.5.25",
25
+ "@team-agent/cli-linux-x64": "0.5.25"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",