@team-agent/installer 0.5.50 → 0.5.51
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/adapters.rs +5 -3
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +32 -14
- package/crates/team-agent/src/cli/named_address.rs +11 -48
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +44 -1169
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
- package/crates/team-agent/src/mcp_server/tools.rs +14 -5
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +10 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +101 -49
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -3,6 +3,29 @@ use crate::transport::test_support::OfflineTransport;
|
|
|
3
3
|
use serde_json::json;
|
|
4
4
|
use serial_test::serial;
|
|
5
5
|
|
|
6
|
+
fn launch_source() -> String {
|
|
7
|
+
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/lifecycle");
|
|
8
|
+
let mut files = vec![src.join("launch.rs")];
|
|
9
|
+
let mut siblings = std::fs::read_dir(src.join("launch"))
|
|
10
|
+
.expect("read launch module directory")
|
|
11
|
+
.map(|entry| entry.expect("read launch module entry").path())
|
|
12
|
+
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("rs"))
|
|
13
|
+
.collect::<Vec<_>>();
|
|
14
|
+
siblings.sort();
|
|
15
|
+
files.extend(siblings);
|
|
16
|
+
|
|
17
|
+
files
|
|
18
|
+
.into_iter()
|
|
19
|
+
.map(|path| {
|
|
20
|
+
format!(
|
|
21
|
+
"\n// @source {}\n{}",
|
|
22
|
+
path.strip_prefix(&src).unwrap().display(),
|
|
23
|
+
std::fs::read_to_string(&path).expect("read launch source")
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
.collect()
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
#[allow(dead_code)]
|
|
7
30
|
struct HermeticTestEnv;
|
|
8
31
|
|
|
@@ -968,7 +991,7 @@ fn e5_add_agent_resolves_team_dir_to_role_dir_when_runtime_spec_exists() {
|
|
|
968
991
|
// never write team.spec.yaml into the user team_dir/agents_dir. Pins the spec-demote invariant.
|
|
969
992
|
#[test]
|
|
970
993
|
fn e5_guard_g1_writers_use_runtime_spec_path_not_user_dir() {
|
|
971
|
-
let source =
|
|
994
|
+
let source = launch_source();
|
|
972
995
|
let runtime_writes = source.matches("runtime_spec_path(").count();
|
|
973
996
|
assert!(
|
|
974
997
|
runtime_writes >= 2,
|
|
@@ -989,7 +1012,7 @@ fn e5_guard_g1_writers_use_runtime_spec_path_not_user_dir() {
|
|
|
989
1012
|
// Pins the Bug1 fix: no `fs::copy` of a role file + no `materialize_added_role_file` reborn.
|
|
990
1013
|
#[test]
|
|
991
1014
|
fn e5_guard_g2_no_copy_role_into_platform_dir() {
|
|
992
|
-
let source =
|
|
1015
|
+
let source = launch_source();
|
|
993
1016
|
assert!(
|
|
994
1017
|
!source.contains("materialize_added_role_file"),
|
|
995
1018
|
"G2: materialize_added_role_file (role copy anti-pattern) must stay deleted"
|
|
@@ -3117,6 +3140,36 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
3117
3140
|
assert_eq!(spawn_event["spawned_at"], json!(FIXED_SPAWNED_AT));
|
|
3118
3141
|
assert_eq!(spawn_event["source"], json!("launch"));
|
|
3119
3142
|
assert_eq!(spawn_event["spawn_epoch"], json!(0));
|
|
3143
|
+
let actual_keys = spawn_event
|
|
3144
|
+
.as_object()
|
|
3145
|
+
.expect("spawn event object")
|
|
3146
|
+
.keys()
|
|
3147
|
+
.map(String::as_str)
|
|
3148
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
3149
|
+
let expected_keys = [
|
|
3150
|
+
"agent_id",
|
|
3151
|
+
"argv",
|
|
3152
|
+
"env_overlay_keys",
|
|
3153
|
+
"env_unset",
|
|
3154
|
+
"event",
|
|
3155
|
+
"expected_session_id",
|
|
3156
|
+
"provider",
|
|
3157
|
+
"session_id_in_argv",
|
|
3158
|
+
"source",
|
|
3159
|
+
"spawn_cwd",
|
|
3160
|
+
"spawn_epoch",
|
|
3161
|
+
"spawned_at",
|
|
3162
|
+
"tmux_endpoint",
|
|
3163
|
+
"tmux_endpoint_source",
|
|
3164
|
+
"tmux_start_mode",
|
|
3165
|
+
"ts",
|
|
3166
|
+
]
|
|
3167
|
+
.into_iter()
|
|
3168
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
3169
|
+
assert_eq!(
|
|
3170
|
+
actual_keys, expected_keys,
|
|
3171
|
+
"spawn event schema must be canonical"
|
|
3172
|
+
);
|
|
3120
3173
|
}
|
|
3121
3174
|
|
|
3122
3175
|
// Stage B2 — golden launch/core.py:171-173 writes paused workers as exactly
|
|
@@ -233,7 +233,7 @@ fn r2_lifecycle_lock_exists_precondition() {
|
|
|
233
233
|
"remove rollback runs while remove-agent holds the lifecycle lock; it must use lock-free start_agent_at_paths"
|
|
234
234
|
);
|
|
235
235
|
|
|
236
|
-
let launch =
|
|
236
|
+
let launch = launch_source();
|
|
237
237
|
assert_public_operation(&launch, "add-agent");
|
|
238
238
|
assert_public_operation(&launch, "fork-agent");
|
|
239
239
|
assert_body_is_unlocked(&launch, "fn add_agent_with_transport_at_paths");
|
|
@@ -279,6 +279,29 @@ fn read_src(path: &str) -> String {
|
|
|
279
279
|
std::fs::read_to_string(manifest_src().join(path)).unwrap()
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
fn launch_source() -> String {
|
|
283
|
+
let src = manifest_src().join("lifecycle");
|
|
284
|
+
let mut files = vec![src.join("launch.rs")];
|
|
285
|
+
let mut siblings = std::fs::read_dir(src.join("launch"))
|
|
286
|
+
.expect("read launch module directory")
|
|
287
|
+
.map(|entry| entry.expect("read launch module entry").path())
|
|
288
|
+
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("rs"))
|
|
289
|
+
.collect::<Vec<_>>();
|
|
290
|
+
siblings.sort();
|
|
291
|
+
files.extend(siblings);
|
|
292
|
+
|
|
293
|
+
files
|
|
294
|
+
.into_iter()
|
|
295
|
+
.map(|path| {
|
|
296
|
+
format!(
|
|
297
|
+
"\n// @source {}\n{}",
|
|
298
|
+
path.strip_prefix(&src).unwrap().display(),
|
|
299
|
+
std::fs::read_to_string(&path).expect("read launch source")
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
.collect()
|
|
303
|
+
}
|
|
304
|
+
|
|
282
305
|
fn source_files(root: &Path) -> Vec<PathBuf> {
|
|
283
306
|
let mut out = Vec::new();
|
|
284
307
|
collect_rs_files(root, &mut out);
|
|
@@ -252,12 +252,28 @@ fn state_spec_workspace_from_entry(state: &Value) -> Option<PathBuf> {
|
|
|
252
252
|
|
|
253
253
|
fn load_local_runtime_state(workspace: &Path) -> Result<Value, super::super::ToolError> {
|
|
254
254
|
let path = crate::state::persist::runtime_state_path(workspace);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
tool_runtime_error(format!(
|
|
260
|
-
|
|
255
|
+
match crate::state::repository::StateRepository::new(workspace)
|
|
256
|
+
.load_workspace_if_exists_without_migrations()
|
|
257
|
+
{
|
|
258
|
+
Ok(Some(state)) => Ok(state),
|
|
259
|
+
Ok(None) => Err(tool_runtime_error(format!(
|
|
260
|
+
"read local runtime state {}: {}",
|
|
261
|
+
path.display(),
|
|
262
|
+
std::io::Error::from(std::io::ErrorKind::NotFound)
|
|
263
|
+
))),
|
|
264
|
+
Err(crate::state::StateError::Json(error)) => Err(tool_runtime_error(format!(
|
|
265
|
+
"parse local runtime state {}: {error}",
|
|
266
|
+
path.display()
|
|
267
|
+
))),
|
|
268
|
+
Err(crate::state::StateError::Io(error)) => Err(tool_runtime_error(format!(
|
|
269
|
+
"read local runtime state {}: {error}",
|
|
270
|
+
path.display()
|
|
271
|
+
))),
|
|
272
|
+
Err(error) => Err(tool_runtime_error(format!(
|
|
273
|
+
"read local runtime state {}: {error}",
|
|
274
|
+
path.display()
|
|
275
|
+
))),
|
|
276
|
+
}
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
fn materialize_mcp_lifecycle_spec(
|
|
@@ -416,7 +432,14 @@ fn prepare_selected_team_state(
|
|
|
416
432
|
}
|
|
417
433
|
}
|
|
418
434
|
}
|
|
419
|
-
crate::state::
|
|
435
|
+
crate::state::repository::StateRepository::new(workspace)
|
|
436
|
+
.save(
|
|
437
|
+
crate::state::repository::StateWriteIntent::McpLifecycleAgentOps {
|
|
438
|
+
team_key: Some(team),
|
|
439
|
+
},
|
|
440
|
+
state,
|
|
441
|
+
)
|
|
442
|
+
.map_err(|e| {
|
|
420
443
|
tool_runtime_error(format!(
|
|
421
444
|
"save MCP lifecycle scoped state {}: {e}",
|
|
422
445
|
workspace.display()
|
|
@@ -26,8 +26,10 @@ pub(crate) fn update_state(
|
|
|
26
26
|
let mut state = selected.state;
|
|
27
27
|
ensure_object(&mut state);
|
|
28
28
|
append_note(&mut state, note);
|
|
29
|
-
crate::state::
|
|
30
|
-
|
|
29
|
+
crate::state::repository::StateRepository::new(&selected.run_workspace).save_reapplying(
|
|
30
|
+
crate::state::repository::StateWriteIntent::McpUpdateStateNote {
|
|
31
|
+
team_key: Some(&selected.team_key),
|
|
32
|
+
},
|
|
31
33
|
&state,
|
|
32
34
|
|latest| {
|
|
33
35
|
ensure_object(latest);
|
|
@@ -66,8 +68,10 @@ fn update_state_without_spec(
|
|
|
66
68
|
ensure_object(&mut state);
|
|
67
69
|
seed_legacy_team_key(&mut state, &selected.run_workspace, &selected.team_key);
|
|
68
70
|
append_note(&mut state, note);
|
|
69
|
-
crate::state::
|
|
70
|
-
|
|
71
|
+
crate::state::repository::StateRepository::new(&selected.run_workspace).save_reapplying(
|
|
72
|
+
crate::state::repository::StateWriteIntent::McpUpdateStateNote {
|
|
73
|
+
team_key: Some(&selected.team_key),
|
|
74
|
+
},
|
|
71
75
|
&state,
|
|
72
76
|
|latest| {
|
|
73
77
|
ensure_object(latest);
|
|
@@ -85,7 +85,7 @@ use crate::event_log::EventLog;
|
|
|
85
85
|
use crate::message_store::MessageStore;
|
|
86
86
|
|
|
87
87
|
// ── REUSE: step 5 state persist / projection ────────────────────────────────
|
|
88
|
-
use crate::state::persist::
|
|
88
|
+
use crate::state::persist::load_runtime_state;
|
|
89
89
|
|
|
90
90
|
// ── REUSE: step 11 messaging delegate surface ───────────────────────────────
|
|
91
91
|
use crate::messaging::{self, DeliveryOutcome, MessageTarget, SendOptions, TrustedSender};
|
|
@@ -319,20 +319,16 @@ fn send_message_worker_recipient_surfaces_dead_coordinator_warning() {
|
|
|
319
319
|
None,
|
|
320
320
|
None,
|
|
321
321
|
)
|
|
322
|
-
.expect("send
|
|
322
|
+
.expect("send persists a durable blocker, not an MCP error");
|
|
323
323
|
let v = outcome.to_value();
|
|
324
|
-
assert_eq!(v.get("status"), Some(&json!("
|
|
325
|
-
assert_eq!(v.get("reason"), Some(&json!("coordinator_unavailable")));
|
|
324
|
+
assert_eq!(v.get("status"), Some(&json!("accepted")));
|
|
326
325
|
assert!(
|
|
327
|
-
v.get("
|
|
326
|
+
v.get("message_id")
|
|
328
327
|
.and_then(Value::as_str)
|
|
329
|
-
.is_some_and(|
|
|
330
|
-
"
|
|
331
|
-
);
|
|
332
|
-
assert!(
|
|
333
|
-
v.get("delivery_pending").is_none(),
|
|
334
|
-
"dead coordinator must not return the old accepted async envelope"
|
|
328
|
+
.is_some_and(|id| id.starts_with("msg_")),
|
|
329
|
+
"worker MCP send must expose the durable row id; value={v}"
|
|
335
330
|
);
|
|
331
|
+
assert_eq!(v.get("delivery_pending"), Some(&json!(true)));
|
|
336
332
|
}
|
|
337
333
|
|
|
338
334
|
#[test]
|
|
@@ -126,11 +126,19 @@ impl TeamOrchestratorTools {
|
|
|
126
126
|
.map(|team| team.as_str().to_string())
|
|
127
127
|
.or_else(|| assignment_team_key(&state));
|
|
128
128
|
reconcile_assigned_task(&mut state, team_key.as_deref(), &task_value);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
crate::state::repository::StateRepository::new(&self.workspace)
|
|
130
|
+
.save_reapplying(
|
|
131
|
+
crate::state::repository::StateWriteIntent::McpAssignTask {
|
|
132
|
+
team_key: team_key.as_deref(),
|
|
133
|
+
task_id,
|
|
134
|
+
},
|
|
135
|
+
&state,
|
|
136
|
+
|latest| {
|
|
137
|
+
ensure_object(latest);
|
|
138
|
+
let latest_team_key = team_key.clone().or_else(|| assignment_team_key(latest));
|
|
139
|
+
reconcile_assigned_task(latest, latest_team_key.as_deref(), &task_value);
|
|
140
|
+
},
|
|
141
|
+
)
|
|
134
142
|
.map_err(tool_runtime_error)?;
|
|
135
143
|
|
|
136
144
|
let content = assignment_message(task, message);
|
|
@@ -213,6 +221,7 @@ impl TeamOrchestratorTools {
|
|
|
213
221
|
)
|
|
214
222
|
.map_err(tool_runtime_error)?;
|
|
215
223
|
let opts = SendOptions {
|
|
224
|
+
origin: crate::messaging::SendOrigin::Mcp,
|
|
216
225
|
task_id: task_id.map(TaskId::new),
|
|
217
226
|
route_task_id: true,
|
|
218
227
|
sender,
|
|
@@ -80,8 +80,10 @@ pub fn detect_idle_fallbacks(
|
|
|
80
80
|
let suppression_snapshots =
|
|
81
81
|
idle_fallback_suppression_snapshots(&next_state, store, &team, &idle_workers)?;
|
|
82
82
|
register_idle_fallback_suppression(&mut next_state, &team, &now, &suppression_snapshots);
|
|
83
|
-
crate::state::
|
|
84
|
-
|
|
83
|
+
crate::state::repository::StateRepository::new(workspace).save_reapplying(
|
|
84
|
+
crate::state::repository::StateWriteIntent::MessagingTurnArm {
|
|
85
|
+
owner_team_id: Some(&team),
|
|
86
|
+
},
|
|
85
87
|
&next_state,
|
|
86
88
|
|latest| {
|
|
87
89
|
register_idle_fallback_suppression(latest, &team, &now, &suppression_snapshots);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//! Pure logical-address grammar shared by public entry-point adapters.
|
|
2
|
+
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
6
|
+
pub struct LogicalAddress {
|
|
7
|
+
pub workspace: Option<PathBuf>,
|
|
8
|
+
pub target: LogicalAddressTarget,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
12
|
+
pub enum LogicalAddressTarget {
|
|
13
|
+
Worker(String),
|
|
14
|
+
TeamEntity { team: String, entity: String },
|
|
15
|
+
SessionWindow { session: String, window: String },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn parse_logical_address(raw_name: &str) -> Result<LogicalAddress, &'static str> {
|
|
19
|
+
let raw = raw_name.trim();
|
|
20
|
+
if raw.is_empty() {
|
|
21
|
+
return Err("name is empty");
|
|
22
|
+
}
|
|
23
|
+
let (workspace, name) = if let Some((workspace, rest)) = raw.split_once("::") {
|
|
24
|
+
if workspace.trim().is_empty() || rest.trim().is_empty() {
|
|
25
|
+
return Err("workspace-qualified name must include workspace and target");
|
|
26
|
+
}
|
|
27
|
+
(Some(PathBuf::from(workspace)), rest.trim())
|
|
28
|
+
} else {
|
|
29
|
+
(None, raw)
|
|
30
|
+
};
|
|
31
|
+
if name.contains("//") {
|
|
32
|
+
return Err("name contains an empty path segment");
|
|
33
|
+
}
|
|
34
|
+
let target = if name.contains('/') {
|
|
35
|
+
let parts = name.split('/').collect::<Vec<_>>();
|
|
36
|
+
if parts.len() != 2 || !valid_component(parts[0]) || !valid_component(parts[1]) {
|
|
37
|
+
return Err("expected <team>/<agent> or <team>/leader");
|
|
38
|
+
}
|
|
39
|
+
LogicalAddressTarget::TeamEntity {
|
|
40
|
+
team: parts[0].to_string(),
|
|
41
|
+
entity: parts[1].to_string(),
|
|
42
|
+
}
|
|
43
|
+
} else if name.contains(':') {
|
|
44
|
+
let parts = name.split(':').collect::<Vec<_>>();
|
|
45
|
+
if parts.len() != 2 || parts[0].trim().is_empty() || parts[1].trim().is_empty() {
|
|
46
|
+
return Err("expected <session>:<window>");
|
|
47
|
+
}
|
|
48
|
+
LogicalAddressTarget::SessionWindow {
|
|
49
|
+
session: parts[0].to_string(),
|
|
50
|
+
window: parts[1].to_string(),
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
if !valid_component(name) {
|
|
54
|
+
return Err("expected a non-empty agent id");
|
|
55
|
+
}
|
|
56
|
+
LogicalAddressTarget::Worker(name.to_string())
|
|
57
|
+
};
|
|
58
|
+
Ok(LogicalAddress { workspace, target })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn valid_component(raw: &str) -> bool {
|
|
62
|
+
!raw.trim().is_empty()
|
|
63
|
+
&& !raw.contains(char::is_whitespace)
|
|
64
|
+
&& !raw.contains('/')
|
|
65
|
+
&& !raw.contains(':')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[cfg(test)]
|
|
69
|
+
mod tests {
|
|
70
|
+
use super::*;
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn parses_canonical_send_grammar_without_runtime_state() {
|
|
74
|
+
assert_eq!(
|
|
75
|
+
parse_logical_address("/tmp/ws::team/w1").unwrap(),
|
|
76
|
+
LogicalAddress {
|
|
77
|
+
workspace: Some(PathBuf::from("/tmp/ws")),
|
|
78
|
+
target: LogicalAddressTarget::TeamEntity {
|
|
79
|
+
team: "team".into(),
|
|
80
|
+
entity: "w1".into(),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
assert!(parse_logical_address("team//w1").is_err());
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -45,20 +45,28 @@ pub fn deliver_stored_message(
|
|
|
45
45
|
) -> Result<DeliveryOutcome, MessagingError> {
|
|
46
46
|
let _ = (wait_visible, timeout);
|
|
47
47
|
let recipient = target.unwrap_or("leader");
|
|
48
|
-
let
|
|
49
|
-
|
|
48
|
+
let super::PersistResolution::Persisted(persisted) = super::persist::persist_internal_send(
|
|
49
|
+
workspace,
|
|
50
|
+
super::InternalSendKind::Delivery,
|
|
51
|
+
team.map(TeamKey::as_str),
|
|
50
52
|
task_id.map(crate::model::ids::TaskId::as_str),
|
|
51
53
|
sender,
|
|
52
54
|
recipient,
|
|
53
55
|
content,
|
|
54
56
|
None,
|
|
55
57
|
requires_ack,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
None,
|
|
59
|
+
super::InitialDisposition::Accepted,
|
|
60
|
+
)?
|
|
61
|
+
else {
|
|
62
|
+
unreachable!("internal delivery does not accept caller-supplied ids")
|
|
63
|
+
};
|
|
64
|
+
let message_status = persisted.row_status.as_str().to_string();
|
|
65
|
+
let message_id = persisted.message_id;
|
|
58
66
|
Ok(DeliveryOutcome {
|
|
59
67
|
ok: true,
|
|
60
68
|
status: DeliveryStatus::Queued,
|
|
61
|
-
message_status: MessageStatusShadow(
|
|
69
|
+
message_status: MessageStatusShadow(message_status),
|
|
62
70
|
message_id: Some(message_id),
|
|
63
71
|
verification: None,
|
|
64
72
|
stage: None,
|
|
@@ -67,6 +75,17 @@ pub fn deliver_stored_message(
|
|
|
67
75
|
})
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
pub fn deliver_persisted_message(
|
|
79
|
+
workspace: &Path,
|
|
80
|
+
transport: &dyn Transport,
|
|
81
|
+
message_id: &str,
|
|
82
|
+
event_log: &EventLog,
|
|
83
|
+
state: &serde_json::Value,
|
|
84
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
85
|
+
let store = MessageStore::open(workspace)?;
|
|
86
|
+
deliver_pending_message(workspace, &store, transport, message_id, event_log, state)
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
// ===========================================================================
|
|
71
90
|
// delivery.py — 单条 tmux 注入投递 + trust 有界重试 + turn-open arm (card §16)
|
|
72
91
|
// ===========================================================================
|
|
@@ -200,6 +219,22 @@ pub fn deliver_pending_message(
|
|
|
200
219
|
}
|
|
201
220
|
_ => state,
|
|
202
221
|
};
|
|
222
|
+
if message.recipient == "leader" && message.status == "submitted_pending_acceptance" {
|
|
223
|
+
return observe_pending_leader_acceptance(store, event_log, state, message_id);
|
|
224
|
+
}
|
|
225
|
+
if message.recipient == "leader" && message.status == "queued_until_leader_attach" {
|
|
226
|
+
if leader_receiver_status(state) != Some("attached") {
|
|
227
|
+
return Ok(leader_acceptance_pending_outcome(
|
|
228
|
+
message_id,
|
|
229
|
+
"queued_until_leader_attach",
|
|
230
|
+
));
|
|
231
|
+
}
|
|
232
|
+
store.mark(message_id, "accepted", None)?;
|
|
233
|
+
event_log.write(
|
|
234
|
+
"leader_receiver.mailbox_requeued",
|
|
235
|
+
serde_json::json!({"message_id": message_id}),
|
|
236
|
+
)?;
|
|
237
|
+
}
|
|
203
238
|
let attempt = if store.claim_for_delivery(message_id)? {
|
|
204
239
|
message.delivery_attempts.saturating_add(1)
|
|
205
240
|
} else if message.status == "target_resolved" && message.error.is_some() {
|
|
@@ -524,14 +559,10 @@ pub fn deliver_pending_message(
|
|
|
524
559
|
}
|
|
525
560
|
let submit_verified = inject_submit_verified(&inject_report);
|
|
526
561
|
let readback_verified = pane_readback_verified(&inject_report);
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
let verified =
|
|
531
|
-
true
|
|
532
|
-
} else {
|
|
533
|
-
submit_verified
|
|
534
|
-
};
|
|
562
|
+
// A successful tmux command is never provider-acceptance proof. Leader and
|
|
563
|
+
// worker delivery both require post-submit evidence; leader additionally
|
|
564
|
+
// waits for its authoritative provider transcript below.
|
|
565
|
+
let verified = submit_verified;
|
|
535
566
|
if !verified {
|
|
536
567
|
let reason = if !readback_verified {
|
|
537
568
|
"pane_readback_unverified:capture_missing_token".to_string()
|
|
@@ -658,7 +689,26 @@ pub fn deliver_pending_message(
|
|
|
658
689
|
});
|
|
659
690
|
}
|
|
660
691
|
}
|
|
661
|
-
|
|
692
|
+
if is_leader_recipient {
|
|
693
|
+
store.record_delivery_submission(message_id, readback_verified)?;
|
|
694
|
+
if !leader_transcript_has_token(state, message_id) {
|
|
695
|
+
store.mark(message_id, "submitted_pending_acceptance", None)?;
|
|
696
|
+
event_log.write(
|
|
697
|
+
"leader_receiver.acceptance_pending",
|
|
698
|
+
serde_json::json!({
|
|
699
|
+
"message_id": message_id,
|
|
700
|
+
"reason": "provider_receipt_not_observed",
|
|
701
|
+
}),
|
|
702
|
+
)?;
|
|
703
|
+
return Ok(leader_acceptance_pending_outcome(
|
|
704
|
+
message_id,
|
|
705
|
+
"submitted_pending_acceptance",
|
|
706
|
+
));
|
|
707
|
+
}
|
|
708
|
+
store.mark_delivered_with_receipt(message_id)?;
|
|
709
|
+
} else {
|
|
710
|
+
store.mark(message_id, "delivered", None)?;
|
|
711
|
+
}
|
|
662
712
|
let mut delivered_event = serde_json::json!({"message_id": message_id});
|
|
663
713
|
if let Some(metadata) = resolved.metadata.as_ref() {
|
|
664
714
|
append_target_metadata(&mut delivered_event, metadata);
|
|
@@ -908,7 +958,8 @@ fn deliver_leader_via_app_server(
|
|
|
908
958
|
);
|
|
909
959
|
match crate::codex_app_server::submit_to_bound_thread(&binding, message_id, &rendered) {
|
|
910
960
|
Ok(submit) => {
|
|
911
|
-
store.
|
|
961
|
+
store.record_delivery_submission(message_id, true)?;
|
|
962
|
+
store.mark_delivered_with_receipt(message_id)?;
|
|
912
963
|
event_log.write(
|
|
913
964
|
"message.delivered",
|
|
914
965
|
serde_json::json!({"message_id": message_id}),
|
|
@@ -1800,10 +1851,16 @@ pub fn deliver_pending_messages(
|
|
|
1800
1851
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
1801
1852
|
let mut stmt = conn.prepare(
|
|
1802
1853
|
"select message_id from messages
|
|
1803
|
-
where status in (
|
|
1854
|
+
where status in (
|
|
1855
|
+
'pending', 'accepted', 'target_resolved',
|
|
1856
|
+
'submitted_pending_acceptance', 'queued_until_leader_attach', ?1
|
|
1857
|
+
)
|
|
1804
1858
|
order by created_at, message_id",
|
|
1805
1859
|
)?;
|
|
1806
|
-
let rows = stmt.query_map(
|
|
1860
|
+
let rows = stmt.query_map(
|
|
1861
|
+
[crate::message_store::MessageRowStatus::QueuedCoordinatorUnavailable.as_str()],
|
|
1862
|
+
|row| row.get::<_, String>(0),
|
|
1863
|
+
)?;
|
|
1807
1864
|
rows.collect::<Result<Vec<_>, _>>()?
|
|
1808
1865
|
};
|
|
1809
1866
|
let mut delivered = Vec::new();
|
|
@@ -1874,7 +1931,7 @@ pub fn deliver_pending_messages(
|
|
|
1874
1931
|
continue;
|
|
1875
1932
|
}
|
|
1876
1933
|
};
|
|
1877
|
-
if outcome.
|
|
1934
|
+
if matches!(outcome.status, DeliveryStatus::Delivered) {
|
|
1878
1935
|
delivered.push(message_id);
|
|
1879
1936
|
}
|
|
1880
1937
|
}
|
|
@@ -2017,6 +2074,73 @@ fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
|
|
|
2017
2074
|
== Some("busy")
|
|
2018
2075
|
}
|
|
2019
2076
|
|
|
2077
|
+
fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf> {
|
|
2078
|
+
state
|
|
2079
|
+
.get("leader")
|
|
2080
|
+
.and_then(|leader| leader.get("rollout_path"))
|
|
2081
|
+
.and_then(serde_json::Value::as_str)
|
|
2082
|
+
.or_else(|| {
|
|
2083
|
+
leader_receiver_value(state)
|
|
2084
|
+
.and_then(|receiver| receiver.get("rollout_path"))
|
|
2085
|
+
.and_then(serde_json::Value::as_str)
|
|
2086
|
+
})
|
|
2087
|
+
.filter(|path| !path.is_empty())
|
|
2088
|
+
.map(std::path::PathBuf::from)
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
pub(crate) fn leader_transcript_has_token(state: &serde_json::Value, message_id: &str) -> bool {
|
|
2092
|
+
let Some(path) = leader_rollout_path(state) else {
|
|
2093
|
+
return false;
|
|
2094
|
+
};
|
|
2095
|
+
rollout_tail_contains(
|
|
2096
|
+
&path,
|
|
2097
|
+
&format!("[team-agent-token:{message_id}]"),
|
|
2098
|
+
64 * 1024,
|
|
2099
|
+
)
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
fn leader_acceptance_pending_outcome(message_id: &str, status: &str) -> DeliveryOutcome {
|
|
2103
|
+
DeliveryOutcome {
|
|
2104
|
+
ok: true,
|
|
2105
|
+
status: DeliveryStatus::RetryScheduled,
|
|
2106
|
+
message_status: MessageStatusShadow(status.to_string()),
|
|
2107
|
+
message_id: Some(message_id.to_string()),
|
|
2108
|
+
verification: Some("provider_receipt_not_observed".to_string()),
|
|
2109
|
+
stage: Some(DeliveryStage::Submit),
|
|
2110
|
+
reason: None,
|
|
2111
|
+
channel: Some("leader_acceptance_pending".to_string()),
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
fn observe_pending_leader_acceptance(
|
|
2116
|
+
store: &MessageStore,
|
|
2117
|
+
event_log: &EventLog,
|
|
2118
|
+
state: &serde_json::Value,
|
|
2119
|
+
message_id: &str,
|
|
2120
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
2121
|
+
if !leader_transcript_has_token(state, message_id) {
|
|
2122
|
+
return Ok(leader_acceptance_pending_outcome(
|
|
2123
|
+
message_id,
|
|
2124
|
+
"submitted_pending_acceptance",
|
|
2125
|
+
));
|
|
2126
|
+
}
|
|
2127
|
+
store.mark_delivered_with_receipt(message_id)?;
|
|
2128
|
+
event_log.write(
|
|
2129
|
+
"message.delivered",
|
|
2130
|
+
serde_json::json!({"message_id": message_id}),
|
|
2131
|
+
)?;
|
|
2132
|
+
Ok(DeliveryOutcome {
|
|
2133
|
+
ok: true,
|
|
2134
|
+
status: DeliveryStatus::Delivered,
|
|
2135
|
+
message_status: MessageStatusShadow("delivered".to_string()),
|
|
2136
|
+
message_id: Some(message_id.to_string()),
|
|
2137
|
+
verification: Some("provider_transcript_receipt".to_string()),
|
|
2138
|
+
stage: None,
|
|
2139
|
+
reason: None,
|
|
2140
|
+
channel: Some("leader_receiver".to_string()),
|
|
2141
|
+
})
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2020
2144
|
/// `_handle_trust_retry_needed` (`delivery.py:221`):trust 应答失败时调度有界退避重试
|
|
2021
2145
|
/// (`attempt < MAX` → schedule;`>= MAX` → 终态 mark failed + `trust_auto_answer_exhausted`)。
|
|
2022
2146
|
pub fn handle_trust_retry_needed(
|
|
@@ -2341,8 +2465,10 @@ fn save_scoped_state(
|
|
|
2341
2465
|
state: &serde_json::Value,
|
|
2342
2466
|
owner_team_id: Option<&str>,
|
|
2343
2467
|
) -> Result<(), MessagingError> {
|
|
2344
|
-
|
|
2345
|
-
|
|
2468
|
+
let scoped_owner_team_id = owner_team_id
|
|
2469
|
+
.filter(|team| !team.is_empty())
|
|
2470
|
+
.filter(|_| {
|
|
2471
|
+
state
|
|
2346
2472
|
.get("teams")
|
|
2347
2473
|
.and_then(serde_json::Value::as_object)
|
|
2348
2474
|
.is_some_and(|teams| {
|
|
@@ -2354,14 +2480,13 @@ fn save_scoped_state(
|
|
|
2354
2480
|
})
|
|
2355
2481
|
.is_some_and(|team| teams.contains_key(&team))
|
|
2356
2482
|
})
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
}
|
|
2483
|
+
});
|
|
2484
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
2485
|
+
crate::state::repository::StateWriteIntent::MessagingDeliveryState {
|
|
2486
|
+
owner_team_id: scoped_owner_team_id,
|
|
2487
|
+
},
|
|
2488
|
+
state,
|
|
2489
|
+
)?;
|
|
2365
2490
|
Ok(())
|
|
2366
2491
|
}
|
|
2367
2492
|
|
|
@@ -2374,8 +2499,10 @@ fn save_scoped_state_reapplying_after_conflict<F>(
|
|
|
2374
2499
|
where
|
|
2375
2500
|
F: FnOnce(&mut serde_json::Value),
|
|
2376
2501
|
{
|
|
2377
|
-
|
|
2378
|
-
|
|
2502
|
+
let scoped_owner_team_id = owner_team_id
|
|
2503
|
+
.filter(|team| !team.is_empty())
|
|
2504
|
+
.filter(|_| {
|
|
2505
|
+
state
|
|
2379
2506
|
.get("teams")
|
|
2380
2507
|
.and_then(serde_json::Value::as_object)
|
|
2381
2508
|
.is_some_and(|teams| {
|
|
@@ -2387,15 +2514,14 @@ where
|
|
|
2387
2514
|
})
|
|
2388
2515
|
.is_some_and(|team| teams.contains_key(&team))
|
|
2389
2516
|
})
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
}
|
|
2517
|
+
});
|
|
2518
|
+
crate::state::repository::StateRepository::new(workspace).save_reapplying(
|
|
2519
|
+
crate::state::repository::StateWriteIntent::MessagingDeliveryState {
|
|
2520
|
+
owner_team_id: scoped_owner_team_id,
|
|
2521
|
+
},
|
|
2522
|
+
state,
|
|
2523
|
+
reapply,
|
|
2524
|
+
)?;
|
|
2399
2525
|
Ok(())
|
|
2400
2526
|
}
|
|
2401
2527
|
|