@team-agent/installer 0.5.50 → 0.5.52
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 +46 -115
- 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/main_preserved.rs +1 -0
- 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/lease.rs +202 -16
- 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 +303 -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 +63 -29
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
- package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
- package/crates/team-agent/src/lifecycle/restart.rs +26 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -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/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- 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 +288 -183
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +15 -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_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +2 -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 +284 -21
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +94 -27
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/package.json +4 -4
|
@@ -52,6 +52,8 @@ pub enum MessageStoreError {
|
|
|
52
52
|
Sqlite(#[from] rusqlite::Error),
|
|
53
53
|
#[error("io: {0}")]
|
|
54
54
|
Io(#[from] std::io::Error),
|
|
55
|
+
#[error("delivery receipt missing for message: {0}")]
|
|
56
|
+
DeliveryReceiptMissing(String),
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/// Outcome of [`MessageStore::claim_leader_notification_delivery`]
|
|
@@ -78,6 +80,40 @@ pub struct NotificationClaimParams<'a> {
|
|
|
78
80
|
pub pane_id: Option<&'a str>,
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
/// Canonical initial message-row statuses shared by persistence, presentation,
|
|
84
|
+
/// claiming and recovery. New durable dispositions must be added here first.
|
|
85
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
86
|
+
pub enum MessageRowStatus {
|
|
87
|
+
Accepted,
|
|
88
|
+
QueuedUntilLeaderAttach,
|
|
89
|
+
QueuedCoordinatorUnavailable,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl MessageRowStatus {
|
|
93
|
+
pub const fn as_str(self) -> &'static str {
|
|
94
|
+
match self {
|
|
95
|
+
Self::Accepted => "accepted",
|
|
96
|
+
Self::QueuedUntilLeaderAttach => "queued_until_leader_attach",
|
|
97
|
+
Self::QueuedCoordinatorUnavailable => "queued_coordinator_unavailable",
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Fully resolved durable-message insert. Grammar, scope and transport data do
|
|
103
|
+
/// not belong here; callers must resolve those before crossing this boundary.
|
|
104
|
+
pub struct PersistMessageInput<'a> {
|
|
105
|
+
pub message_id: Option<&'a str>,
|
|
106
|
+
pub owner_team_id: Option<&'a str>,
|
|
107
|
+
pub task_id: Option<&'a str>,
|
|
108
|
+
pub sender: &'a str,
|
|
109
|
+
pub recipient: &'a str,
|
|
110
|
+
pub reply_to: Option<&'a str>,
|
|
111
|
+
pub requires_ack: bool,
|
|
112
|
+
pub status: MessageRowStatus,
|
|
113
|
+
pub content: &'a str,
|
|
114
|
+
pub error: Option<&'a str>,
|
|
115
|
+
}
|
|
116
|
+
|
|
81
117
|
/// `leader_notification_log._legacy_epoch_from_uuid` (line 145-147):
|
|
82
118
|
/// `int(zlib.crc32(str(uuid or "").encode("utf-8")) & 0x7FFFFFFF)`.
|
|
83
119
|
pub fn legacy_epoch_from_uuid(leader_session_uuid: Option<&str>) -> i64 {
|
|
@@ -133,44 +169,67 @@ impl MessageStore {
|
|
|
133
169
|
&self.path
|
|
134
170
|
}
|
|
135
171
|
|
|
136
|
-
|
|
137
|
-
/// a row with `status='accepted'`, `requires_ack` as 0/1 int, `artifact_refs`
|
|
138
|
-
/// defaulting to `'[]'`, `delivery_attempts=0`, timestamps = now.
|
|
139
|
-
#[allow(clippy::too_many_arguments)]
|
|
140
|
-
pub fn create_message(
|
|
172
|
+
pub fn persist_message(
|
|
141
173
|
&self,
|
|
142
|
-
|
|
143
|
-
sender: &str,
|
|
144
|
-
recipient: &str,
|
|
145
|
-
content: &str,
|
|
146
|
-
reply_to: Option<&str>,
|
|
147
|
-
requires_ack: bool,
|
|
148
|
-
owner_team_id: Option<&str>,
|
|
174
|
+
input: PersistMessageInput<'_>,
|
|
149
175
|
) -> Result<String, MessageStoreError> {
|
|
150
176
|
let conn = crate::db::schema::open_db(&self.path)?;
|
|
151
|
-
let message_id =
|
|
177
|
+
let message_id = input
|
|
178
|
+
.message_id
|
|
179
|
+
.map(ToOwned::to_owned)
|
|
180
|
+
.unwrap_or_else(next_message_id);
|
|
152
181
|
let now = now_ts();
|
|
153
182
|
conn.execute(
|
|
154
183
|
"insert into messages(
|
|
155
184
|
message_id, owner_team_id, task_id, sender, recipient, reply_to, requires_ack,
|
|
156
185
|
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
157
186
|
acknowledged_at, error, delivery_attempts
|
|
158
|
-
) values (?1, ?2, ?3, ?4, ?5, ?6, ?7,
|
|
187
|
+
) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, '[]', ?10, ?10, null, null, ?11, 0)",
|
|
159
188
|
params![
|
|
160
189
|
message_id,
|
|
161
|
-
owner_team_id,
|
|
162
|
-
task_id,
|
|
163
|
-
sender,
|
|
164
|
-
recipient,
|
|
165
|
-
reply_to,
|
|
166
|
-
if requires_ack { 1 } else { 0 },
|
|
167
|
-
|
|
190
|
+
input.owner_team_id,
|
|
191
|
+
input.task_id,
|
|
192
|
+
input.sender,
|
|
193
|
+
input.recipient,
|
|
194
|
+
input.reply_to,
|
|
195
|
+
if input.requires_ack { 1 } else { 0 },
|
|
196
|
+
input.status.as_str(),
|
|
197
|
+
input.content,
|
|
168
198
|
now,
|
|
199
|
+
input.error,
|
|
169
200
|
],
|
|
170
201
|
)?;
|
|
171
202
|
Ok(message_id)
|
|
172
203
|
}
|
|
173
204
|
|
|
205
|
+
/// `create_message` (`core.py:71-114`). Returns `msg_<uuid4 hex[:12]>`; inserts
|
|
206
|
+
/// a row with `status='accepted'`, `requires_ack` as 0/1 int, `artifact_refs`
|
|
207
|
+
/// defaulting to `'[]'`, `delivery_attempts=0`, timestamps = now.
|
|
208
|
+
#[allow(clippy::too_many_arguments)]
|
|
209
|
+
pub fn create_message(
|
|
210
|
+
&self,
|
|
211
|
+
task_id: Option<&str>,
|
|
212
|
+
sender: &str,
|
|
213
|
+
recipient: &str,
|
|
214
|
+
content: &str,
|
|
215
|
+
reply_to: Option<&str>,
|
|
216
|
+
requires_ack: bool,
|
|
217
|
+
owner_team_id: Option<&str>,
|
|
218
|
+
) -> Result<String, MessageStoreError> {
|
|
219
|
+
self.persist_message(PersistMessageInput {
|
|
220
|
+
message_id: None,
|
|
221
|
+
owner_team_id,
|
|
222
|
+
task_id,
|
|
223
|
+
sender,
|
|
224
|
+
recipient,
|
|
225
|
+
reply_to,
|
|
226
|
+
requires_ack,
|
|
227
|
+
status: MessageRowStatus::Accepted,
|
|
228
|
+
content,
|
|
229
|
+
error: None,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
174
233
|
/// Caller-supplied-id variant of [`create_message`] (CR-015/054 — `--message-id`).
|
|
175
234
|
/// Inserts exactly the given `message_id` instead of generating one. The store
|
|
176
235
|
/// PK is `message_id`, so a repeat with the same id is rejected by SQLite; the
|
|
@@ -190,27 +249,18 @@ impl MessageStore {
|
|
|
190
249
|
requires_ack: bool,
|
|
191
250
|
owner_team_id: Option<&str>,
|
|
192
251
|
) -> Result<String, MessageStoreError> {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
sender,
|
|
206
|
-
recipient,
|
|
207
|
-
reply_to,
|
|
208
|
-
if requires_ack { 1 } else { 0 },
|
|
209
|
-
content,
|
|
210
|
-
now,
|
|
211
|
-
],
|
|
212
|
-
)?;
|
|
213
|
-
Ok(message_id.to_string())
|
|
252
|
+
self.persist_message(PersistMessageInput {
|
|
253
|
+
message_id: Some(message_id),
|
|
254
|
+
owner_team_id,
|
|
255
|
+
task_id,
|
|
256
|
+
sender,
|
|
257
|
+
recipient,
|
|
258
|
+
reply_to,
|
|
259
|
+
requires_ack,
|
|
260
|
+
status: MessageRowStatus::Accepted,
|
|
261
|
+
content,
|
|
262
|
+
error: None,
|
|
263
|
+
})
|
|
214
264
|
}
|
|
215
265
|
|
|
216
266
|
/// `true` iff a `messages` row with this `message_id` already exists. Used by
|
|
@@ -260,6 +310,62 @@ impl MessageStore {
|
|
|
260
310
|
Ok(())
|
|
261
311
|
}
|
|
262
312
|
|
|
313
|
+
/// Record that the transport submitted a message without claiming that the
|
|
314
|
+
/// provider accepted it. The stable message id is also the receipt token.
|
|
315
|
+
pub fn record_delivery_submission(
|
|
316
|
+
&self,
|
|
317
|
+
message_id: &str,
|
|
318
|
+
visible: bool,
|
|
319
|
+
) -> Result<(), MessageStoreError> {
|
|
320
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
321
|
+
let now = now_ts();
|
|
322
|
+
conn.execute(
|
|
323
|
+
"insert into delivery_tokens(
|
|
324
|
+
message_id, unique_token, injected_at, visible_at,
|
|
325
|
+
consumed_at, failed_at, failure_reason
|
|
326
|
+
) values (?1, ?1, ?2, ?3, null, null, null)
|
|
327
|
+
on conflict(message_id) do update set
|
|
328
|
+
visible_at = coalesce(delivery_tokens.visible_at, excluded.visible_at),
|
|
329
|
+
failed_at = null,
|
|
330
|
+
failure_reason = null",
|
|
331
|
+
params![message_id, now, visible.then_some(now_ts())],
|
|
332
|
+
)?;
|
|
333
|
+
Ok(())
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// Atomically persist the provider-side receipt and advance the message to
|
|
337
|
+
/// delivered. A transport-only caller cannot use this without first
|
|
338
|
+
/// recording the submission row above.
|
|
339
|
+
pub fn mark_delivered_with_receipt(&self, message_id: &str) -> Result<(), MessageStoreError> {
|
|
340
|
+
let mut conn = crate::db::schema::open_db(&self.path)?;
|
|
341
|
+
let tx = conn.transaction()?;
|
|
342
|
+
let now = now_ts();
|
|
343
|
+
let receipts = tx.execute(
|
|
344
|
+
"update delivery_tokens
|
|
345
|
+
set consumed_at = coalesce(consumed_at, ?2),
|
|
346
|
+
failed_at = null,
|
|
347
|
+
failure_reason = null
|
|
348
|
+
where message_id = ?1",
|
|
349
|
+
params![message_id, now],
|
|
350
|
+
)?;
|
|
351
|
+
if receipts != 1 {
|
|
352
|
+
return Err(MessageStoreError::DeliveryReceiptMissing(
|
|
353
|
+
message_id.to_string(),
|
|
354
|
+
));
|
|
355
|
+
}
|
|
356
|
+
tx.execute(
|
|
357
|
+
"update messages
|
|
358
|
+
set status = case when status = 'acknowledged' then status else 'delivered' end,
|
|
359
|
+
updated_at = ?2,
|
|
360
|
+
delivered_at = ?2,
|
|
361
|
+
error = null
|
|
362
|
+
where message_id = ?1",
|
|
363
|
+
params![message_id, now_ts()],
|
|
364
|
+
)?;
|
|
365
|
+
tx.commit()?;
|
|
366
|
+
Ok(())
|
|
367
|
+
}
|
|
368
|
+
|
|
263
369
|
/// `claim_for_delivery` (`core.py:190-205`): atomic single-winner claim. Flips an
|
|
264
370
|
/// eligible row (status ∈ pending/accepted/queued_until_idle/queued_until_start/
|
|
265
371
|
/// queued_stopped/queued_pane_missing) to `target_resolved`, `delivery_attempts +=
|
|
@@ -274,9 +380,13 @@ impl MessageStore {
|
|
|
274
380
|
where message_id = ?1
|
|
275
381
|
and status in (
|
|
276
382
|
'pending', 'accepted', 'queued_until_idle', 'queued_until_start',
|
|
277
|
-
'queued_stopped', 'queued_pane_missing'
|
|
383
|
+
'queued_stopped', 'queued_pane_missing', ?3
|
|
278
384
|
)",
|
|
279
|
-
params![
|
|
385
|
+
params![
|
|
386
|
+
message_id,
|
|
387
|
+
now_ts(),
|
|
388
|
+
MessageRowStatus::QueuedCoordinatorUnavailable.as_str()
|
|
389
|
+
],
|
|
280
390
|
)?;
|
|
281
391
|
Ok(rows == 1)
|
|
282
392
|
}
|
|
@@ -35,6 +35,38 @@ use crate::model::paths::logs_dir;
|
|
|
35
35
|
pub const EVENT_LOG_ROTATE_BYTES: u64 = 5 * 1024 * 1024;
|
|
36
36
|
/// `events.py:18`:保留 5 个 archive。
|
|
37
37
|
pub const EVENT_LOG_ARCHIVE_KEEP: u32 = 5;
|
|
38
|
+
pub(crate) const PROVIDER_SESSION_CONVERGING: &str = "provider.session.converging";
|
|
39
|
+
pub(crate) const PROVIDER_WORKER_SPAWN_ARGV: &str = "provider.worker.spawn_argv";
|
|
40
|
+
|
|
41
|
+
pub(crate) fn provider_worker_spawn_argv_fields(fields: Value) -> Value {
|
|
42
|
+
let object = fields.as_object();
|
|
43
|
+
let field = |key: &str| {
|
|
44
|
+
object
|
|
45
|
+
.and_then(|object| object.get(key))
|
|
46
|
+
.cloned()
|
|
47
|
+
.unwrap_or(Value::Null)
|
|
48
|
+
};
|
|
49
|
+
let array = |key: &str| match field(key) {
|
|
50
|
+
value @ Value::Array(_) => value,
|
|
51
|
+
_ => Value::Array(Vec::new()),
|
|
52
|
+
};
|
|
53
|
+
serde_json::json!({
|
|
54
|
+
"agent_id": field("agent_id"),
|
|
55
|
+
"provider": field("provider"),
|
|
56
|
+
"argv": array("argv"),
|
|
57
|
+
"session_id_in_argv": field("session_id_in_argv"),
|
|
58
|
+
"expected_session_id": field("expected_session_id"),
|
|
59
|
+
"spawn_cwd": field("spawn_cwd"),
|
|
60
|
+
"spawned_at": field("spawned_at"),
|
|
61
|
+
"source": field("source"),
|
|
62
|
+
"spawn_epoch": field("spawn_epoch"),
|
|
63
|
+
"env_overlay_keys": array("env_overlay_keys"),
|
|
64
|
+
"env_unset": array("env_unset"),
|
|
65
|
+
"tmux_start_mode": field("tmux_start_mode"),
|
|
66
|
+
"tmux_endpoint": field("tmux_endpoint"),
|
|
67
|
+
"tmux_endpoint_source": field("tmux_endpoint_source"),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
38
70
|
|
|
39
71
|
#[derive(Debug, Error)]
|
|
40
72
|
pub enum EventLogError {
|
|
@@ -277,6 +309,47 @@ mod tests {
|
|
|
277
309
|
assert!(ts.ends_with("+00:00"));
|
|
278
310
|
}
|
|
279
311
|
|
|
312
|
+
#[test]
|
|
313
|
+
fn provider_spawn_fields_have_one_canonical_shape() {
|
|
314
|
+
let fields = provider_worker_spawn_argv_fields(json!({
|
|
315
|
+
"agent_id": "worker",
|
|
316
|
+
"provider": "fake",
|
|
317
|
+
"source": "restart",
|
|
318
|
+
"ignored": "not part of the schema",
|
|
319
|
+
}));
|
|
320
|
+
let object = fields.as_object().unwrap();
|
|
321
|
+
let keys = object
|
|
322
|
+
.keys()
|
|
323
|
+
.map(String::as_str)
|
|
324
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
325
|
+
assert_eq!(
|
|
326
|
+
keys,
|
|
327
|
+
[
|
|
328
|
+
"agent_id",
|
|
329
|
+
"argv",
|
|
330
|
+
"env_overlay_keys",
|
|
331
|
+
"env_unset",
|
|
332
|
+
"expected_session_id",
|
|
333
|
+
"provider",
|
|
334
|
+
"session_id_in_argv",
|
|
335
|
+
"source",
|
|
336
|
+
"spawn_cwd",
|
|
337
|
+
"spawn_epoch",
|
|
338
|
+
"spawned_at",
|
|
339
|
+
"tmux_endpoint",
|
|
340
|
+
"tmux_endpoint_source",
|
|
341
|
+
"tmux_start_mode",
|
|
342
|
+
]
|
|
343
|
+
.into_iter()
|
|
344
|
+
.collect::<std::collections::BTreeSet<_>>()
|
|
345
|
+
);
|
|
346
|
+
assert_eq!(fields["argv"], json!([]));
|
|
347
|
+
assert_eq!(fields["env_overlay_keys"], json!([]));
|
|
348
|
+
assert_eq!(fields["env_unset"], json!([]));
|
|
349
|
+
assert!(fields["spawn_cwd"].is_null());
|
|
350
|
+
assert!(fields.get("ignored").is_none());
|
|
351
|
+
}
|
|
352
|
+
|
|
280
353
|
#[test]
|
|
281
354
|
fn tail_returns_last_n_and_raw_on_bad_line() {
|
|
282
355
|
let ws = temp_ws();
|
|
@@ -712,25 +712,46 @@ fn claim_lease_no_incident_with_target(
|
|
|
712
712
|
candidate_source,
|
|
713
713
|
pre_epoch,
|
|
714
714
|
);
|
|
715
|
-
|
|
715
|
+
let observation_refreshed = caller_pane_info.is_some_and(|pane_info| {
|
|
716
|
+
refresh_already_bound_receiver_observation(
|
|
717
|
+
state,
|
|
718
|
+
pane_info,
|
|
719
|
+
observed_endpoint.or(current_endpoint.as_deref()),
|
|
720
|
+
scoped_team.or(team),
|
|
721
|
+
)
|
|
722
|
+
});
|
|
723
|
+
if converged || observation_refreshed {
|
|
716
724
|
write_claim_state(workspace, state, scoped_team, team)?;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
{
|
|
729
|
-
|
|
725
|
+
if observation_refreshed {
|
|
726
|
+
event_log.write(
|
|
727
|
+
"leader_receiver.observation_refreshed",
|
|
728
|
+
json!({
|
|
729
|
+
"pane_id": caller_pane.as_str(),
|
|
730
|
+
"owner_epoch": pre_epoch.0,
|
|
731
|
+
"owner_changed": false,
|
|
732
|
+
"source": "claim_leader_already_bound",
|
|
733
|
+
}),
|
|
734
|
+
)?;
|
|
735
|
+
}
|
|
736
|
+
if converged {
|
|
737
|
+
topology_convergence = verify_persisted_topology_convergence(
|
|
738
|
+
workspace,
|
|
739
|
+
team_id.as_str(),
|
|
730
740
|
topology_convergence,
|
|
731
741
|
pre_epoch,
|
|
732
|
-
|
|
733
|
-
|
|
742
|
+
)?;
|
|
743
|
+
if topology_convergence
|
|
744
|
+
.as_ref()
|
|
745
|
+
.and_then(|metadata| metadata.get("status"))
|
|
746
|
+
.and_then(Value::as_str)
|
|
747
|
+
!= Some("converged")
|
|
748
|
+
{
|
|
749
|
+
return Ok(convergence_persistence_refusal(
|
|
750
|
+
topology_convergence,
|
|
751
|
+
pre_epoch,
|
|
752
|
+
Some(caller_pane.clone()),
|
|
753
|
+
));
|
|
754
|
+
}
|
|
734
755
|
}
|
|
735
756
|
}
|
|
736
757
|
return Ok(LeaseResult {
|
|
@@ -1659,6 +1680,108 @@ fn receiver_fingerprint(target: &PaneInfo) -> String {
|
|
|
1659
1680
|
)
|
|
1660
1681
|
}
|
|
1661
1682
|
|
|
1683
|
+
fn refresh_already_bound_receiver_observation(
|
|
1684
|
+
state: &mut Value,
|
|
1685
|
+
observed: &PaneInfo,
|
|
1686
|
+
observed_endpoint: Option<&str>,
|
|
1687
|
+
team_key: Option<&str>,
|
|
1688
|
+
) -> bool {
|
|
1689
|
+
let attached_at = now_ts();
|
|
1690
|
+
let mut refreshed = state
|
|
1691
|
+
.get_mut("leader_receiver")
|
|
1692
|
+
.and_then(Value::as_object_mut)
|
|
1693
|
+
.is_some_and(|receiver| {
|
|
1694
|
+
refresh_receiver_observation(
|
|
1695
|
+
receiver,
|
|
1696
|
+
observed,
|
|
1697
|
+
observed_endpoint,
|
|
1698
|
+
attached_at.as_str(),
|
|
1699
|
+
)
|
|
1700
|
+
});
|
|
1701
|
+
if let Some(team_key) = team_key {
|
|
1702
|
+
refreshed |= state
|
|
1703
|
+
.get_mut("teams")
|
|
1704
|
+
.and_then(|teams| teams.get_mut(team_key))
|
|
1705
|
+
.and_then(|entry| entry.get_mut("leader_receiver"))
|
|
1706
|
+
.and_then(Value::as_object_mut)
|
|
1707
|
+
.is_some_and(|receiver| {
|
|
1708
|
+
refresh_receiver_observation(
|
|
1709
|
+
receiver,
|
|
1710
|
+
observed,
|
|
1711
|
+
observed_endpoint,
|
|
1712
|
+
attached_at.as_str(),
|
|
1713
|
+
)
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
refreshed
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
fn refresh_receiver_observation(
|
|
1720
|
+
receiver: &mut serde_json::Map<String, Value>,
|
|
1721
|
+
observed: &PaneInfo,
|
|
1722
|
+
observed_endpoint: Option<&str>,
|
|
1723
|
+
attached_at: &str,
|
|
1724
|
+
) -> bool {
|
|
1725
|
+
let before = Value::Object(receiver.clone());
|
|
1726
|
+
receiver.insert(
|
|
1727
|
+
"session_name".to_string(),
|
|
1728
|
+
Value::String(observed.session.as_str().to_string()),
|
|
1729
|
+
);
|
|
1730
|
+
receiver.insert(
|
|
1731
|
+
"window_index".to_string(),
|
|
1732
|
+
observed
|
|
1733
|
+
.window_index
|
|
1734
|
+
.map(|value| Value::String(value.to_string()))
|
|
1735
|
+
.unwrap_or(Value::Null),
|
|
1736
|
+
);
|
|
1737
|
+
receiver.insert(
|
|
1738
|
+
"window_name".to_string(),
|
|
1739
|
+
observed
|
|
1740
|
+
.window_name
|
|
1741
|
+
.as_ref()
|
|
1742
|
+
.map(|value| Value::String(value.as_str().to_string()))
|
|
1743
|
+
.unwrap_or(Value::Null),
|
|
1744
|
+
);
|
|
1745
|
+
receiver.insert(
|
|
1746
|
+
"pane_index".to_string(),
|
|
1747
|
+
observed
|
|
1748
|
+
.pane_index
|
|
1749
|
+
.map(|value| Value::String(value.to_string()))
|
|
1750
|
+
.unwrap_or(Value::Null),
|
|
1751
|
+
);
|
|
1752
|
+
receiver.insert(
|
|
1753
|
+
"pane_tty".to_string(),
|
|
1754
|
+
observed
|
|
1755
|
+
.tty
|
|
1756
|
+
.as_ref()
|
|
1757
|
+
.map(|value| Value::String(value.clone()))
|
|
1758
|
+
.unwrap_or(Value::Null),
|
|
1759
|
+
);
|
|
1760
|
+
receiver.insert(
|
|
1761
|
+
"pane_current_command".to_string(),
|
|
1762
|
+
observed
|
|
1763
|
+
.current_command
|
|
1764
|
+
.as_ref()
|
|
1765
|
+
.map(|value| Value::String(value.clone()))
|
|
1766
|
+
.unwrap_or(Value::Null),
|
|
1767
|
+
);
|
|
1768
|
+
receiver.insert(
|
|
1769
|
+
"fingerprint".to_string(),
|
|
1770
|
+
Value::String(receiver_fingerprint(observed)),
|
|
1771
|
+
);
|
|
1772
|
+
if let Some(endpoint) = observed_endpoint.filter(|value| !value.is_empty()) {
|
|
1773
|
+
receiver.insert(
|
|
1774
|
+
"tmux_socket".to_string(),
|
|
1775
|
+
Value::String(endpoint.to_string()),
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
receiver.insert(
|
|
1779
|
+
"attached_at".to_string(),
|
|
1780
|
+
Value::String(attached_at.to_string()),
|
|
1781
|
+
);
|
|
1782
|
+
Value::Object(receiver.clone()) != before
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1662
1785
|
fn make_owner(
|
|
1663
1786
|
provider: Provider,
|
|
1664
1787
|
pane: &NonEmptyPaneId,
|
|
@@ -2124,6 +2247,22 @@ fn readable_team_snapshot_path(w: &BP, n: &str) -> BF /* B0_DIAGNOSTIC_LEGACY_SN
|
|
|
2124
2247
|
mod tests {
|
|
2125
2248
|
use super::*;
|
|
2126
2249
|
|
|
2250
|
+
fn observed_pane() -> PaneInfo {
|
|
2251
|
+
PaneInfo {
|
|
2252
|
+
pane_id: PaneId::new("%7"),
|
|
2253
|
+
session: crate::transport::SessionName::new("new-session"),
|
|
2254
|
+
window_index: Some(3),
|
|
2255
|
+
window_name: Some(crate::transport::WindowName::new("new-window")),
|
|
2256
|
+
pane_index: Some(1),
|
|
2257
|
+
tty: Some("/dev/ttys007".to_string()),
|
|
2258
|
+
current_command: Some("codex".to_string()),
|
|
2259
|
+
current_path: None,
|
|
2260
|
+
active: true,
|
|
2261
|
+
pane_pid: Some(700),
|
|
2262
|
+
leader_env: std::collections::BTreeMap::new(),
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2127
2266
|
#[test]
|
|
2128
2267
|
fn attribute_command_provider_recognizes_copilot() {
|
|
2129
2268
|
assert_eq!(
|
|
@@ -2135,4 +2274,51 @@ mod tests {
|
|
|
2135
2274
|
Some(Provider::Copilot)
|
|
2136
2275
|
);
|
|
2137
2276
|
}
|
|
2277
|
+
|
|
2278
|
+
#[test]
|
|
2279
|
+
fn already_bound_refresh_updates_observations_without_reclaiming() {
|
|
2280
|
+
let mut state = json!({
|
|
2281
|
+
"leader_receiver": {
|
|
2282
|
+
"status": "attached",
|
|
2283
|
+
"pane_id": "%7",
|
|
2284
|
+
"owner_epoch": 10,
|
|
2285
|
+
"owner_agent_id": "leader",
|
|
2286
|
+
"session_name": "old-session",
|
|
2287
|
+
"pane_tty": "/dev/old",
|
|
2288
|
+
"attached_at": "2026-01-01T00:00:00Z"
|
|
2289
|
+
},
|
|
2290
|
+
"teams": {"team-a": {"leader_receiver": {
|
|
2291
|
+
"status": "attached",
|
|
2292
|
+
"pane_id": "%7",
|
|
2293
|
+
"owner_epoch": 10,
|
|
2294
|
+
"owner_agent_id": "leader",
|
|
2295
|
+
"session_name": "old-session",
|
|
2296
|
+
"pane_tty": "/dev/old",
|
|
2297
|
+
"attached_at": "2026-01-01T00:00:00Z"
|
|
2298
|
+
}}}
|
|
2299
|
+
});
|
|
2300
|
+
|
|
2301
|
+
assert!(refresh_already_bound_receiver_observation(
|
|
2302
|
+
&mut state,
|
|
2303
|
+
&observed_pane(),
|
|
2304
|
+
Some("/tmp/team-agent-live.sock"),
|
|
2305
|
+
Some("team-a"),
|
|
2306
|
+
));
|
|
2307
|
+
let receiver = &state["leader_receiver"];
|
|
2308
|
+
assert_eq!(receiver["owner_epoch"], json!(10));
|
|
2309
|
+
assert_eq!(receiver["owner_agent_id"], json!("leader"));
|
|
2310
|
+
assert_eq!(receiver["session_name"], json!("new-session"));
|
|
2311
|
+
assert_eq!(receiver["window_name"], json!("new-window"));
|
|
2312
|
+
assert_eq!(receiver["pane_tty"], json!("/dev/ttys007"));
|
|
2313
|
+
assert_eq!(receiver["tmux_socket"], json!("/tmp/team-agent-live.sock"));
|
|
2314
|
+
assert_ne!(receiver["attached_at"], json!("2026-01-01T00:00:00Z"));
|
|
2315
|
+
assert_eq!(
|
|
2316
|
+
state["teams"]["team-a"]["leader_receiver"]["session_name"],
|
|
2317
|
+
json!("new-session")
|
|
2318
|
+
);
|
|
2319
|
+
assert_eq!(
|
|
2320
|
+
state["teams"]["team-a"]["leader_receiver"]["owner_epoch"],
|
|
2321
|
+
json!(10)
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2138
2324
|
}
|
|
@@ -816,7 +816,13 @@ fn persist_managed_leader_binding(
|
|
|
816
816
|
.with_team_owner(owner)
|
|
817
817
|
.with_owner_epoch(owner_epoch);
|
|
818
818
|
crate::state::ownership::write_owner(&mut state, identity.team_id.as_str(), record);
|
|
819
|
-
crate::state::
|
|
819
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
820
|
+
crate::state::repository::StateWriteIntent::LeaderStartBinding {
|
|
821
|
+
team_key: identity.team_id.as_str(),
|
|
822
|
+
transport_kind: "managed",
|
|
823
|
+
},
|
|
824
|
+
&state,
|
|
825
|
+
)?;
|
|
820
826
|
Ok(())
|
|
821
827
|
}
|
|
822
828
|
|
|
@@ -933,7 +939,13 @@ fn persist_exec_provider_leader_binding(
|
|
|
933
939
|
.with_team_owner(owner)
|
|
934
940
|
.with_owner_epoch(owner_epoch);
|
|
935
941
|
crate::state::ownership::write_owner(&mut state, identity.team_id.as_str(), record);
|
|
936
|
-
crate::state::
|
|
942
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
943
|
+
crate::state::repository::StateWriteIntent::LeaderStartBinding {
|
|
944
|
+
team_key: identity.team_id.as_str(),
|
|
945
|
+
transport_kind: "exec_provider",
|
|
946
|
+
},
|
|
947
|
+
&state,
|
|
948
|
+
)?;
|
|
937
949
|
Ok(())
|
|
938
950
|
}
|
|
939
951
|
|
|
@@ -1124,7 +1136,13 @@ fn refresh_managed_leader_provider_binding(
|
|
|
1124
1136
|
.with_team_owner(owner)
|
|
1125
1137
|
.with_owner_epoch(existing_epoch);
|
|
1126
1138
|
crate::state::ownership::write_owner(&mut state, team_key, record);
|
|
1127
|
-
crate::state::
|
|
1139
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
1140
|
+
crate::state::repository::StateWriteIntent::LeaderStartBinding {
|
|
1141
|
+
team_key,
|
|
1142
|
+
transport_kind: "managed_reentry",
|
|
1143
|
+
},
|
|
1144
|
+
&state,
|
|
1145
|
+
)?;
|
|
1128
1146
|
Ok(())
|
|
1129
1147
|
}
|
|
1130
1148
|
|
|
@@ -1169,7 +1187,13 @@ fn persist_external_leader_topology_marker(
|
|
|
1169
1187
|
teams.insert(identity.team_id.as_str().to_string(), entry);
|
|
1170
1188
|
}
|
|
1171
1189
|
}
|
|
1172
|
-
crate::state::
|
|
1190
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
1191
|
+
crate::state::repository::StateWriteIntent::LeaderStartBinding {
|
|
1192
|
+
team_key: identity.team_id.as_str(),
|
|
1193
|
+
transport_kind: "external",
|
|
1194
|
+
},
|
|
1195
|
+
&state,
|
|
1196
|
+
)?;
|
|
1173
1197
|
Ok(())
|
|
1174
1198
|
}
|
|
1175
1199
|
|