@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
|
@@ -10,7 +10,11 @@ use crate::transport::{PaneId, Transport};
|
|
|
10
10
|
|
|
11
11
|
use super::helpers::{status_wire, MessageStatusShadow};
|
|
12
12
|
use super::leader_receiver::{send_to_leader_receiver, send_to_leader_receiver_with_message_id};
|
|
13
|
-
use super::{
|
|
13
|
+
use super::{
|
|
14
|
+
persist_resolved_send, DeliveryBlocker, DeliveryOutcome, DeliveryRefusal, DeliveryStatus,
|
|
15
|
+
InitialDisposition, LogicalRecipient, MessagingError, PersistResolution, ResolvedSendIntent,
|
|
16
|
+
SendOrigin,
|
|
17
|
+
};
|
|
14
18
|
|
|
15
19
|
/// 发件目标:单 target / 广播 `*` / 扇出 list (`send.py:36` `target: str|list[str]|None`)。
|
|
16
20
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -49,6 +53,7 @@ impl TrustedSender {
|
|
|
49
53
|
/// `send_message` 选项 (`send.py:36`:Python 大量默认参数 → typed 选项 struct)。
|
|
50
54
|
#[derive(Debug, Clone)]
|
|
51
55
|
pub struct SendOptions {
|
|
56
|
+
pub origin: SendOrigin,
|
|
52
57
|
pub task_id: Option<TaskId>,
|
|
53
58
|
/// `route_task_id`(golden `send.py:190`,默认 `True`):仅 **routing**(CLI `send --task`)时把
|
|
54
59
|
/// `task_id` 当真任务校验/路由。**投递/fanout/internal/coordinator** 路径传 `false`
|
|
@@ -73,6 +78,7 @@ impl Default for SendOptions {
|
|
|
73
78
|
fn default() -> Self {
|
|
74
79
|
// 默认值对齐 Python 签名 (`send.py:38-40`)。
|
|
75
80
|
Self {
|
|
81
|
+
origin: SendOrigin::Internal(super::InternalSendKind::Delivery),
|
|
76
82
|
task_id: None,
|
|
77
83
|
route_task_id: true,
|
|
78
84
|
sender: TrustedSender::leader(),
|
|
@@ -207,46 +213,47 @@ pub fn send_message(
|
|
|
207
213
|
}
|
|
208
214
|
let stale_worker_target_missing =
|
|
209
215
|
stale_worker_target_missing_preflight(workspace, &state, recipient)?;
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
let coordinator_unavailable = if stale_worker_target_missing {
|
|
217
|
+
None
|
|
218
|
+
} else {
|
|
219
|
+
coordinator_unavailable_outcome(workspace, recipient, opts, &event_log)?
|
|
220
|
+
};
|
|
221
|
+
if let Some(gate) = coordinator_unavailable
|
|
222
|
+
.as_ref()
|
|
223
|
+
.filter(|gate| !gate.persist)
|
|
224
|
+
{
|
|
225
|
+
return Ok(gate.outcome.clone());
|
|
216
226
|
}
|
|
217
|
-
let
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
let mut intent = ResolvedSendIntent::accepted(
|
|
228
|
+
opts.origin,
|
|
229
|
+
workspace,
|
|
230
|
+
opts.team.clone(),
|
|
231
|
+
LogicalRecipient::from_resolved(recipient),
|
|
232
|
+
opts.sender.clone(),
|
|
233
|
+
opts.task_id.clone(),
|
|
234
|
+
content,
|
|
235
|
+
None,
|
|
236
|
+
opts.requires_ack,
|
|
237
|
+
opts.message_id.clone(),
|
|
238
|
+
);
|
|
239
|
+
if coordinator_unavailable
|
|
240
|
+
.as_ref()
|
|
241
|
+
.is_some_and(|gate| gate.persist)
|
|
242
|
+
{
|
|
243
|
+
intent.initial_disposition =
|
|
244
|
+
InitialDisposition::Blocked(DeliveryBlocker::CoordinatorUnavailable);
|
|
245
|
+
}
|
|
246
|
+
let persisted = match persist_resolved_send(&intent)? {
|
|
247
|
+
PersistResolution::Persisted(persisted) => persisted,
|
|
248
|
+
PersistResolution::Duplicate(message_id) => {
|
|
224
249
|
return Ok(refused_outcome_with_id(
|
|
225
250
|
DeliveryRefusal::Duplicate,
|
|
226
|
-
Some(
|
|
251
|
+
Some(message_id),
|
|
227
252
|
));
|
|
228
253
|
}
|
|
229
|
-
store.create_message_with_id(
|
|
230
|
-
requested,
|
|
231
|
-
task_id,
|
|
232
|
-
opts.sender.as_str(),
|
|
233
|
-
recipient,
|
|
234
|
-
content,
|
|
235
|
-
None,
|
|
236
|
-
opts.requires_ack,
|
|
237
|
-
owner_team_id,
|
|
238
|
-
)?
|
|
239
|
-
} else {
|
|
240
|
-
store.create_message(
|
|
241
|
-
task_id,
|
|
242
|
-
opts.sender.as_str(),
|
|
243
|
-
recipient,
|
|
244
|
-
content,
|
|
245
|
-
None,
|
|
246
|
-
opts.requires_ack,
|
|
247
|
-
owner_team_id,
|
|
248
|
-
)?
|
|
249
254
|
};
|
|
255
|
+
let message_id = persisted.message_id.clone();
|
|
256
|
+
let store = crate::message_store::MessageStore::open(workspace)?;
|
|
250
257
|
if stale_worker_target_missing {
|
|
251
258
|
return super::delivery::mark_worker_target_missing(
|
|
252
259
|
&store,
|
|
@@ -256,10 +263,37 @@ pub fn send_message(
|
|
|
256
263
|
None,
|
|
257
264
|
);
|
|
258
265
|
}
|
|
266
|
+
if let Some(CoordinatorUnavailableGate {
|
|
267
|
+
mut outcome,
|
|
268
|
+
persist: true,
|
|
269
|
+
}) = coordinator_unavailable
|
|
270
|
+
{
|
|
271
|
+
let Some(blocker) = persisted.blocker else {
|
|
272
|
+
return Err(MessagingError::Validation(
|
|
273
|
+
"coordinator-blocked persistence returned no blocker".to_string(),
|
|
274
|
+
));
|
|
275
|
+
};
|
|
276
|
+
let message_status = persisted.row_status.as_str();
|
|
277
|
+
event_log.write(
|
|
278
|
+
"send.message_queued",
|
|
279
|
+
serde_json::json!({
|
|
280
|
+
"message_id": message_id,
|
|
281
|
+
"recipient": recipient,
|
|
282
|
+
"sender": opts.sender,
|
|
283
|
+
"message_status": message_status,
|
|
284
|
+
"blocker": blocker.as_str(),
|
|
285
|
+
}),
|
|
286
|
+
)?;
|
|
287
|
+
outcome.ok = true;
|
|
288
|
+
outcome.status = DeliveryStatus::Blocked;
|
|
289
|
+
outcome.message_status = MessageStatusShadow(message_status.to_string());
|
|
290
|
+
outcome.message_id = Some(message_id);
|
|
291
|
+
return Ok(outcome);
|
|
292
|
+
}
|
|
259
293
|
Ok(DeliveryOutcome {
|
|
260
294
|
ok: true,
|
|
261
295
|
status: DeliveryStatus::Queued,
|
|
262
|
-
message_status: MessageStatusShadow(
|
|
296
|
+
message_status: MessageStatusShadow(persisted.row_status.as_str().to_string()),
|
|
263
297
|
message_id: Some(message_id),
|
|
264
298
|
verification: None,
|
|
265
299
|
stage: None,
|
|
@@ -331,12 +365,17 @@ fn refused_outcome_with_verification(
|
|
|
331
365
|
}
|
|
332
366
|
}
|
|
333
367
|
|
|
368
|
+
struct CoordinatorUnavailableGate {
|
|
369
|
+
outcome: DeliveryOutcome,
|
|
370
|
+
persist: bool,
|
|
371
|
+
}
|
|
372
|
+
|
|
334
373
|
fn coordinator_unavailable_outcome(
|
|
335
374
|
workspace: &Path,
|
|
336
375
|
recipient: &str,
|
|
337
376
|
opts: &SendOptions,
|
|
338
377
|
event_log: &EventLog,
|
|
339
|
-
) -> Result<Option<
|
|
378
|
+
) -> Result<Option<CoordinatorUnavailableGate>, MessagingError> {
|
|
340
379
|
let coordinator_workspace = WorkspacePath::new(workspace.to_path_buf());
|
|
341
380
|
let health = crate::coordinator::coordinator_health(&coordinator_workspace);
|
|
342
381
|
if health.ok || matches!(health.status, CoordinatorHealthStatus::Missing) {
|
|
@@ -366,9 +405,19 @@ fn coordinator_unavailable_outcome(
|
|
|
366
405
|
)?;
|
|
367
406
|
return Ok(None);
|
|
368
407
|
}
|
|
369
|
-
let
|
|
370
|
-
|
|
408
|
+
let persist = !matches!(
|
|
409
|
+
health.metadata_mismatch_reason.as_deref(),
|
|
410
|
+
Some("protocol_version_mismatch" | "message_store_schema_version_mismatch")
|
|
371
411
|
);
|
|
412
|
+
let warning = if persist {
|
|
413
|
+
format!(
|
|
414
|
+
"coordinator is not running; message was queued for {recipient} and will retry. Run `team-agent diagnose` or restart the team."
|
|
415
|
+
)
|
|
416
|
+
} else {
|
|
417
|
+
format!(
|
|
418
|
+
"coordinator protocol or schema is incompatible; message was not queued for {recipient}. Run `team-agent diagnose` or upgrade the runtime."
|
|
419
|
+
)
|
|
420
|
+
};
|
|
372
421
|
event_log.write(
|
|
373
422
|
"send.coordinator_unavailable",
|
|
374
423
|
serde_json::json!({
|
|
@@ -377,22 +426,25 @@ fn coordinator_unavailable_outcome(
|
|
|
377
426
|
"coordinator_status": health.status,
|
|
378
427
|
"coordinator_pid": health.pid.map(|pid| pid.get()),
|
|
379
428
|
"metadata_mismatch_reason": health.metadata_mismatch_reason,
|
|
380
|
-
"message_queued":
|
|
429
|
+
"message_queued": persist,
|
|
381
430
|
"warning": warning,
|
|
382
431
|
"coordinator_log": crate::coordinator::coordinator_log_path(&coordinator_workspace)
|
|
383
432
|
.display()
|
|
384
433
|
.to_string(),
|
|
385
434
|
}),
|
|
386
435
|
)?;
|
|
387
|
-
Ok(Some(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
436
|
+
Ok(Some(CoordinatorUnavailableGate {
|
|
437
|
+
persist,
|
|
438
|
+
outcome: DeliveryOutcome {
|
|
439
|
+
ok: false,
|
|
440
|
+
status: DeliveryStatus::Degraded,
|
|
441
|
+
message_status: MessageStatusShadow("degraded".to_string()),
|
|
442
|
+
message_id: None,
|
|
443
|
+
verification: Some(warning),
|
|
444
|
+
stage: None,
|
|
445
|
+
reason: Some(DeliveryRefusal::CoordinatorUnavailable),
|
|
446
|
+
channel: Some("coordinator_unavailable".to_string()),
|
|
447
|
+
},
|
|
396
448
|
}))
|
|
397
449
|
}
|
|
398
450
|
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
//! Unit regressions for leader injection receipt acceptance.
|
|
2
|
+
//!
|
|
3
|
+
//! These fixtures intentionally seed DB/state/transcript internals. Per MUST-15,
|
|
4
|
+
//! that synthetic setup belongs only in src/**/tests and is not acceptance proof.
|
|
5
|
+
|
|
6
|
+
use super::*;
|
|
7
|
+
use std::collections::BTreeMap;
|
|
8
|
+
use std::path::{Path, PathBuf};
|
|
9
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
10
|
+
use std::sync::{Arc, Mutex};
|
|
11
|
+
|
|
12
|
+
use crate::event_log::EventLog;
|
|
13
|
+
use crate::message_store::MessageStore;
|
|
14
|
+
use crate::transport::{
|
|
15
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
16
|
+
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
|
|
17
|
+
SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
|
|
18
|
+
TurnVerification, WindowName,
|
|
19
|
+
};
|
|
20
|
+
use rusqlite::params;
|
|
21
|
+
|
|
22
|
+
#[test]
|
|
23
|
+
fn tmux_leader_submit_without_provider_receipt_stays_pending_then_accepts_same_row() {
|
|
24
|
+
let case = Case::new("receipt");
|
|
25
|
+
let transport = RecordingTransport::new("");
|
|
26
|
+
let message_id = case.seed_message("receipt canary");
|
|
27
|
+
|
|
28
|
+
let first = deliver_pending_message(
|
|
29
|
+
&case.workspace,
|
|
30
|
+
&case.store,
|
|
31
|
+
&transport,
|
|
32
|
+
&message_id,
|
|
33
|
+
&case.event_log,
|
|
34
|
+
&case.state,
|
|
35
|
+
)
|
|
36
|
+
.expect("first delivery attempt");
|
|
37
|
+
|
|
38
|
+
assert!(
|
|
39
|
+
first.ok,
|
|
40
|
+
"verified transport submission is accepted even while provider receipt is pending: {first:?}"
|
|
41
|
+
);
|
|
42
|
+
assert_eq!(
|
|
43
|
+
case.message_status(&message_id),
|
|
44
|
+
"submitted_pending_acceptance"
|
|
45
|
+
);
|
|
46
|
+
assert_eq!(transport.inject_count(), 1);
|
|
47
|
+
assert!(
|
|
48
|
+
case.events().contains("leader_receiver.acceptance_pending"),
|
|
49
|
+
"missing receipt must be observable"
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
std::fs::write(
|
|
53
|
+
&case.rollout,
|
|
54
|
+
format!(r#"{{"type":"user","message":"[team-agent-token:{message_id}]"}}"#),
|
|
55
|
+
)
|
|
56
|
+
.unwrap();
|
|
57
|
+
let second = deliver_pending_message(
|
|
58
|
+
&case.workspace,
|
|
59
|
+
&case.store,
|
|
60
|
+
&transport,
|
|
61
|
+
&message_id,
|
|
62
|
+
&case.event_log,
|
|
63
|
+
&case.state,
|
|
64
|
+
)
|
|
65
|
+
.expect("receipt observation");
|
|
66
|
+
|
|
67
|
+
assert!(
|
|
68
|
+
second.ok,
|
|
69
|
+
"provider receipt advances the same row: {second:?}"
|
|
70
|
+
);
|
|
71
|
+
assert_eq!(case.message_status(&message_id), "delivered");
|
|
72
|
+
assert_eq!(
|
|
73
|
+
transport.inject_count(),
|
|
74
|
+
1,
|
|
75
|
+
"receipt observation must not inject the message a second time"
|
|
76
|
+
);
|
|
77
|
+
assert!(case.delivery_consumed_at(&message_id).is_some());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn attached_leader_mailbox_is_rechecked_by_the_normal_delivery_tick() {
|
|
82
|
+
let case = Case::new("mailbox");
|
|
83
|
+
let transport = RecordingTransport::new("");
|
|
84
|
+
let message_id = case.seed_message("mailbox canary");
|
|
85
|
+
case.store
|
|
86
|
+
.mark(&message_id, "queued_until_leader_attach", None)
|
|
87
|
+
.unwrap();
|
|
88
|
+
|
|
89
|
+
let delivered =
|
|
90
|
+
deliver_pending_messages(&case.workspace, &case.state, &transport, &case.event_log)
|
|
91
|
+
.expect("normal coordinator delivery pass");
|
|
92
|
+
|
|
93
|
+
assert!(delivered.is_empty(), "provider receipt is still pending");
|
|
94
|
+
assert_eq!(
|
|
95
|
+
case.message_status(&message_id),
|
|
96
|
+
"submitted_pending_acceptance"
|
|
97
|
+
);
|
|
98
|
+
assert_eq!(transport.inject_count(), 1);
|
|
99
|
+
assert!(case.events().contains("leader_receiver.mailbox_requeued"));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
struct Case {
|
|
103
|
+
workspace: PathBuf,
|
|
104
|
+
rollout: PathBuf,
|
|
105
|
+
store: MessageStore,
|
|
106
|
+
event_log: EventLog,
|
|
107
|
+
state: serde_json::Value,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl Case {
|
|
111
|
+
fn new(tag: &str) -> Self {
|
|
112
|
+
let workspace = tmp_ws(&format!("leader-inject-acceptance-{tag}"));
|
|
113
|
+
let rollout = workspace.join("leader.jsonl");
|
|
114
|
+
std::fs::write(&rollout, "").unwrap();
|
|
115
|
+
let state = serde_json::json!({
|
|
116
|
+
"active_team_key": "acceptance-team",
|
|
117
|
+
"session_name": "team-acceptance",
|
|
118
|
+
"leader_receiver": {
|
|
119
|
+
"pane_id": "%leader",
|
|
120
|
+
"status": "attached",
|
|
121
|
+
"provider": "claude",
|
|
122
|
+
"rollout_path": rollout,
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
126
|
+
let store = MessageStore::open(&workspace).unwrap();
|
|
127
|
+
let event_log = EventLog::new(&workspace);
|
|
128
|
+
Self {
|
|
129
|
+
workspace,
|
|
130
|
+
rollout,
|
|
131
|
+
store,
|
|
132
|
+
event_log,
|
|
133
|
+
state,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn seed_message(&self, content: &str) -> String {
|
|
138
|
+
self.store
|
|
139
|
+
.create_message(None, "worker", "leader", content, None, false, None)
|
|
140
|
+
.unwrap()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn message_status(&self, message_id: &str) -> String {
|
|
144
|
+
let conn = crate::db::schema::open_db(self.store.db_path()).unwrap();
|
|
145
|
+
conn.query_row(
|
|
146
|
+
"select status from messages where message_id = ?1",
|
|
147
|
+
[message_id],
|
|
148
|
+
|row| row.get(0),
|
|
149
|
+
)
|
|
150
|
+
.unwrap()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn delivery_consumed_at(&self, message_id: &str) -> Option<String> {
|
|
154
|
+
let conn = crate::db::schema::open_db(self.store.db_path()).unwrap();
|
|
155
|
+
conn.query_row(
|
|
156
|
+
"select consumed_at from delivery_tokens where message_id = ?1",
|
|
157
|
+
params![message_id],
|
|
158
|
+
|row| row.get(0),
|
|
159
|
+
)
|
|
160
|
+
.unwrap()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn events(&self) -> String {
|
|
164
|
+
self.event_log
|
|
165
|
+
.tail(0)
|
|
166
|
+
.unwrap()
|
|
167
|
+
.into_iter()
|
|
168
|
+
.map(|event| event.to_string())
|
|
169
|
+
.collect::<Vec<_>>()
|
|
170
|
+
.join("\n")
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[derive(Clone)]
|
|
175
|
+
struct RecordingTransport {
|
|
176
|
+
injects: Arc<AtomicUsize>,
|
|
177
|
+
capture: Arc<Mutex<String>>,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
impl RecordingTransport {
|
|
181
|
+
fn new(capture: &str) -> Self {
|
|
182
|
+
Self {
|
|
183
|
+
injects: Arc::new(AtomicUsize::new(0)),
|
|
184
|
+
capture: Arc::new(Mutex::new(capture.to_string())),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn inject_count(&self) -> usize {
|
|
189
|
+
self.injects.load(Ordering::Relaxed)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl Transport for RecordingTransport {
|
|
194
|
+
fn kind(&self) -> BackendKind {
|
|
195
|
+
BackendKind::Tmux
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn spawn_first(
|
|
199
|
+
&self,
|
|
200
|
+
_session: &SessionName,
|
|
201
|
+
_window: &WindowName,
|
|
202
|
+
_argv: &[String],
|
|
203
|
+
_cwd: &Path,
|
|
204
|
+
_env: &BTreeMap<String, String>,
|
|
205
|
+
) -> Result<SpawnResult, TransportError> {
|
|
206
|
+
unreachable!("delivery contract does not spawn")
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn spawn_into(
|
|
210
|
+
&self,
|
|
211
|
+
_session: &SessionName,
|
|
212
|
+
_window: &WindowName,
|
|
213
|
+
_argv: &[String],
|
|
214
|
+
_cwd: &Path,
|
|
215
|
+
_env: &BTreeMap<String, String>,
|
|
216
|
+
) -> Result<SpawnResult, TransportError> {
|
|
217
|
+
unreachable!("delivery contract does not spawn")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn inject(
|
|
221
|
+
&self,
|
|
222
|
+
_target: &Target,
|
|
223
|
+
_payload: &InjectPayload,
|
|
224
|
+
_submit: Key,
|
|
225
|
+
_bracketed_paste: bool,
|
|
226
|
+
) -> Result<InjectReport, TransportError> {
|
|
227
|
+
self.injects.fetch_add(1, Ordering::Relaxed);
|
|
228
|
+
Ok(InjectReport {
|
|
229
|
+
stage_reached: InjectStage::Submit,
|
|
230
|
+
inject_verification: InjectVerification::CaptureContainsToken,
|
|
231
|
+
submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
232
|
+
turn_verification: TurnVerification::NotYetObserved,
|
|
233
|
+
attempts: 1,
|
|
234
|
+
submit_diagnostics: None,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
|
|
239
|
+
Ok(())
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn capture(
|
|
243
|
+
&self,
|
|
244
|
+
_target: &Target,
|
|
245
|
+
range: CaptureRange,
|
|
246
|
+
) -> Result<CapturedText, TransportError> {
|
|
247
|
+
Ok(CapturedText {
|
|
248
|
+
text: self.capture.lock().unwrap().clone(),
|
|
249
|
+
range,
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
|
|
254
|
+
Ok(None)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
258
|
+
Ok(PaneLiveness::Live)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
262
|
+
Ok(vec![PaneInfo {
|
|
263
|
+
pane_id: PaneId::new("%leader"),
|
|
264
|
+
session: SessionName::new("team-acceptance"),
|
|
265
|
+
window_index: None,
|
|
266
|
+
window_name: Some(WindowName::new("leader")),
|
|
267
|
+
pane_index: None,
|
|
268
|
+
tty: None,
|
|
269
|
+
current_command: Some("claude".to_string()),
|
|
270
|
+
current_path: None,
|
|
271
|
+
active: true,
|
|
272
|
+
pane_pid: None,
|
|
273
|
+
leader_env: BTreeMap::new(),
|
|
274
|
+
}])
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
278
|
+
Ok(true)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
282
|
+
Ok(vec![WindowName::new("leader")])
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn set_session_env(
|
|
286
|
+
&self,
|
|
287
|
+
_session: &SessionName,
|
|
288
|
+
_key: &str,
|
|
289
|
+
_value: &str,
|
|
290
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
291
|
+
Ok(SetEnvOutcome::Applied)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
295
|
+
Ok(())
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
299
|
+
Ok(())
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
303
|
+
Ok(AttachOutcome::Attached)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -18,12 +18,11 @@ fn pane_info(pane_id: &str, session: &str, window: &str) -> PaneInfo {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// ════════════════════════════════════════════════════════════════════════
|
|
21
|
-
// GROUP E —
|
|
22
|
-
// status=FallbackLog (NOT a real submit). leader.py:394-436.
|
|
21
|
+
// GROUP E — legacy diagnostics may only advance an existing durable row.
|
|
23
22
|
// ════════════════════════════════════════════════════════════════════════
|
|
24
23
|
|
|
25
24
|
#[test]
|
|
26
|
-
fn
|
|
25
|
+
fn fail_leader_delivery_without_message_id_does_not_invent_a_row() {
|
|
27
26
|
let ws = tmp_ws("faillead");
|
|
28
27
|
let payload = json(serde_json::json!({
|
|
29
28
|
"to": "leader", "content": "hi", "sender": "coordinator"
|
|
@@ -35,12 +34,17 @@ fn fail_leader_delivery_returns_fallback_log_ok_true_not_submitted() {
|
|
|
35
34
|
Some("No direct leader tmux pane is attached. Run team-agent attach-leader."),
|
|
36
35
|
)
|
|
37
36
|
.unwrap();
|
|
38
|
-
|
|
39
|
-
assert!(out.ok);
|
|
37
|
+
assert!(!out.ok);
|
|
40
38
|
assert_eq!(out.status, DeliveryStatus::FallbackLog);
|
|
41
39
|
assert_eq!(out.reason, Some(DeliveryRefusal::LeaderNotAttached));
|
|
42
|
-
|
|
40
|
+
assert_eq!(out.message_id, None);
|
|
43
41
|
assert_ne!(out.status, DeliveryStatus::Delivered);
|
|
42
|
+
let store = MessageStore::open(&ws).unwrap();
|
|
43
|
+
let connection = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
44
|
+
let rows: i64 = connection
|
|
45
|
+
.query_row("select count(*) from messages", [], |row| row.get(0))
|
|
46
|
+
.unwrap();
|
|
47
|
+
assert_eq!(rows, 0, "diagnostics cannot create a fallback message row");
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -439,28 +443,30 @@ fn message_not_silently_stuck_accepted_when_coordinator_dead() {
|
|
|
439
443
|
)
|
|
440
444
|
.unwrap();
|
|
441
445
|
|
|
442
|
-
assert!(
|
|
443
|
-
assert_eq!(out.status, DeliveryStatus::
|
|
444
|
-
assert_eq!(out.message_status.0, "
|
|
446
|
+
assert!(out.ok, "durable persistence is the send success boundary");
|
|
447
|
+
assert_eq!(out.status, DeliveryStatus::Blocked);
|
|
448
|
+
assert_eq!(out.message_status.0, "queued_coordinator_unavailable");
|
|
449
|
+
assert!(out.message_id.as_deref().is_some_and(|id| id.starts_with("msg_")));
|
|
445
450
|
assert_eq!(out.reason, Some(DeliveryRefusal::CoordinatorUnavailable));
|
|
446
451
|
assert!(
|
|
447
452
|
out.verification
|
|
448
453
|
.as_deref()
|
|
449
454
|
.is_some_and(|warning| warning.contains("coordinator is not running")),
|
|
450
|
-
"N38 warning must explain
|
|
455
|
+
"N38 warning must explain the durable retry blocker; out={out:?}"
|
|
451
456
|
);
|
|
452
457
|
let store = MessageStore::open(&ws).unwrap();
|
|
453
458
|
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
454
|
-
let
|
|
459
|
+
let blocked: i64 = conn
|
|
455
460
|
.query_row(
|
|
456
|
-
"select count(*) from messages where status = '
|
|
461
|
+
"select count(*) from messages where status = 'queued_coordinator_unavailable' \
|
|
462
|
+
and error = 'coordinator_unavailable' and delivered_at is null",
|
|
457
463
|
[],
|
|
458
464
|
|row| row.get(0),
|
|
459
465
|
)
|
|
460
466
|
.unwrap();
|
|
461
467
|
assert_eq!(
|
|
462
|
-
|
|
463
|
-
"dead coordinator send must
|
|
468
|
+
blocked, 1,
|
|
469
|
+
"dead coordinator send must park exactly one durable, non-delivered row"
|
|
464
470
|
);
|
|
465
471
|
let events = EventLog::new(&ws).tail(20).unwrap();
|
|
466
472
|
assert!(
|
|
@@ -470,7 +476,7 @@ fn message_not_silently_stuck_accepted_when_coordinator_dead() {
|
|
|
470
476
|
&& event
|
|
471
477
|
.get("message_queued")
|
|
472
478
|
.and_then(serde_json::Value::as_bool)
|
|
473
|
-
== Some(
|
|
479
|
+
== Some(true)
|
|
474
480
|
}),
|
|
475
481
|
"send.coordinator_unavailable event must be durable; events={events:?}"
|
|
476
482
|
);
|
|
@@ -2914,9 +2920,15 @@ fn gate054_rebind_required_status_refuses_leader_delivery_without_cross_server_p
|
|
|
2914
2920
|
}
|
|
2915
2921
|
|
|
2916
2922
|
#[test]
|
|
2917
|
-
fn
|
|
2923
|
+
fn gate054_attached_status_still_accepts_when_pane_is_live() {
|
|
2918
2924
|
// Companion negative: prove the new status gate does NOT over-refuse when
|
|
2919
2925
|
// `status="attached"` and the workspace-canonical socket has the pane.
|
|
2926
|
+
// Injection-case revision: transport submit success is NOT a provider
|
|
2927
|
+
// receipt, so the old `delivered` pin was a false-positive lock
|
|
2928
|
+
// (MUST-10: physical submit is the framework's fact; acceptance is the
|
|
2929
|
+
// provider's). The un-refused path now parks the SAME row in
|
|
2930
|
+
// submitted_pending_acceptance until a provider receipt or retry
|
|
2931
|
+
// schedule advances it.
|
|
2920
2932
|
let ws = tmp_ws("gate054ok");
|
|
2921
2933
|
let store = store_for(&ws);
|
|
2922
2934
|
let log = EventLog::new(&ws);
|
|
@@ -2945,7 +2957,16 @@ fn gate054_attached_status_still_delivers_when_pane_is_live() {
|
|
|
2945
2957
|
out.ok,
|
|
2946
2958
|
"0.5.4 gate054: status='attached' with a live pane must not be over-refused: {out:?}"
|
|
2947
2959
|
);
|
|
2948
|
-
assert_eq!(
|
|
2960
|
+
assert_eq!(
|
|
2961
|
+
transport.inject_targets().len(),
|
|
2962
|
+
1,
|
|
2963
|
+
"gate054: exactly one physical inject must have happened (MUST-10 anchor)"
|
|
2964
|
+
);
|
|
2965
|
+
assert_eq!(
|
|
2966
|
+
out.message_status.0, "submitted_pending_acceptance",
|
|
2967
|
+
"gate054 (injection-case revision): a successful physical submit without a provider \
|
|
2968
|
+
receipt must park as submitted_pending_acceptance, never claim delivered: {out:?}"
|
|
2969
|
+
);
|
|
2949
2970
|
}
|
|
2950
2971
|
|
|
2951
2972
|
// ════════════════════════════════════════════════════════════════════════
|