@team-agent/installer 0.5.42 → 0.5.44
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 +129 -54
- package/crates/team-agent/src/cli/diagnose.rs +1 -2
- package/crates/team-agent/src/cli/emit.rs +1 -7
- package/crates/team-agent/src/cli/helpers.rs +3 -1
- package/crates/team-agent/src/cli/leader.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +126 -16
- package/crates/team-agent/src/cli/named_address.rs +14 -5
- package/crates/team-agent/src/cli/profile.rs +19 -7
- package/crates/team-agent/src/cli/send.rs +9 -3
- package/crates/team-agent/src/cli/status.rs +8 -30
- package/crates/team-agent/src/cli/status_port.rs +1341 -1272
- package/crates/team-agent/src/cli/tests/base.rs +738 -660
- package/crates/team-agent/src/cli/tests/compile.rs +45 -18
- package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
- package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
- package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
- package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
- package/crates/team-agent/src/cli/tests/mod.rs +17 -7
- package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
- package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
- package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
- package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
- package/crates/team-agent/src/codex_app_server.rs +2 -5
- package/crates/team-agent/src/compiler/tests.rs +139 -33
- package/crates/team-agent/src/compiler.rs +55 -22
- package/crates/team-agent/src/conpty/backend.rs +23 -33
- package/crates/team-agent/src/coordinator/backoff.rs +2 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
- package/crates/team-agent/src/coordinator/health.rs +46 -31
- package/crates/team-agent/src/coordinator/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/orphan.rs +22 -10
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
- package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
- package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
- package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
- package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
- package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
- package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
- package/crates/team-agent/src/db/message_store.rs +138 -30
- package/crates/team-agent/src/db/migration.rs +249 -61
- package/crates/team-agent/src/db/schema.rs +303 -82
- package/crates/team-agent/src/diagnose/comms.rs +9 -2
- package/crates/team-agent/src/diagnose/mod.rs +1 -3
- package/crates/team-agent/src/diagnose/orphans.rs +79 -61
- package/crates/team-agent/src/event_log.rs +70 -16
- package/crates/team-agent/src/layout/manager.rs +15 -4
- package/crates/team-agent/src/layout/mod.rs +4 -4
- package/crates/team-agent/src/layout/overlay.rs +10 -3
- package/crates/team-agent/src/layout/placement.rs +5 -1
- package/crates/team-agent/src/layout/recovery.rs +4 -2
- package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
- package/crates/team-agent/src/layout/sessions.rs +17 -9
- package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +87 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +199 -89
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
- package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
- package/crates/team-agent/src/leader/rediscover.rs +74 -25
- package/crates/team-agent/src/leader/registry.rs +1 -1
- package/crates/team-agent/src/leader/start.rs +75 -54
- package/crates/team-agent/src/leader/takeover.rs +46 -11
- package/crates/team-agent/src/leader/tests/basics.rs +320 -167
- package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
- package/crates/team-agent/src/leader/tests/identity.rs +428 -356
- package/crates/team-agent/src/leader/tests/idle.rs +285 -254
- package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
- package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
- package/crates/team-agent/src/leader/tests/mod.rs +115 -99
- package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
- package/crates/team-agent/src/lib.rs +4 -4
- package/crates/team-agent/src/lifecycle/display.rs +7 -3
- package/crates/team-agent/src/lifecycle/launch.rs +55 -15
- package/crates/team-agent/src/lifecycle/mod.rs +9 -1
- package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
- package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests.rs +2 -2
- package/crates/team-agent/src/main.rs +4 -4
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
- package/crates/team-agent/src/mcp_server/mod.rs +11 -2
- package/crates/team-agent/src/mcp_server/types.rs +14 -3
- package/crates/team-agent/src/mcp_server/wire.rs +230 -68
- package/crates/team-agent/src/messaging/delivery.rs +20 -18
- package/crates/team-agent/src/messaging/helpers.rs +25 -4
- package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
- package/crates/team-agent/src/messaging/mod.rs +2 -3
- package/crates/team-agent/src/messaging/selftest.rs +46 -26
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
- package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
- package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
- package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
- package/crates/team-agent/src/messaging/trust.rs +25 -2
- package/crates/team-agent/src/messaging/watchers.rs +37 -9
- package/crates/team-agent/src/model/enums.rs +65 -16
- package/crates/team-agent/src/model/ids.rs +12 -3
- package/crates/team-agent/src/model/paths.rs +28 -7
- package/crates/team-agent/src/model/permissions.rs +176 -33
- package/crates/team-agent/src/model/routing.rs +66 -20
- package/crates/team-agent/src/model/spec.rs +365 -69
- package/crates/team-agent/src/model/task_graph.rs +36 -9
- package/crates/team-agent/src/model/yaml/tests.rs +24 -6
- package/crates/team-agent/src/model/yaml.rs +7 -6
- package/crates/team-agent/src/packaging/install.rs +23 -9
- package/crates/team-agent/src/packaging/migrate.rs +5 -7
- package/crates/team-agent/src/packaging/mod.rs +9 -1
- package/crates/team-agent/src/packaging/repair.rs +13 -6
- package/crates/team-agent/src/packaging/tests.rs +63 -16
- package/crates/team-agent/src/packaging/types.rs +22 -7
- package/crates/team-agent/src/platform/argv.rs +4 -1
- package/crates/team-agent/src/platform/file_lock.rs +22 -8
- package/crates/team-agent/src/platform/process.rs +54 -24
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
- package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
- package/crates/team-agent/src/provider/classify.rs +127 -42
- package/crates/team-agent/src/provider/faults.rs +17 -5
- package/crates/team-agent/src/provider/helpers.rs +6 -5
- package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
- package/crates/team-agent/src/state/persist.rs +28 -0
- package/crates/team-agent/src/state/repository.rs +8 -3
- package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
- package/crates/team-agent/src/tmux_backend.rs +80 -44
- package/crates/team-agent/src/topology.rs +40 -20
- package/crates/team-agent/src/transport/test_support.rs +20 -23
- package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
- package/crates/team-agent/src/transport/tests/mod.rs +178 -187
- package/crates/team-agent/src/transport/tests/wire.rs +561 -525
- package/crates/team-agent/src/transport.rs +12 -17
- package/crates/team-agent/src/transport_factory.rs +29 -14
- package/package.json +4 -4
|
@@ -70,11 +70,9 @@ pub fn read_front_matter(path: &Path) -> Result<(Value, String), ModelError> {
|
|
|
70
70
|
let after_meta = rest.get(close..).ok_or_else(|| {
|
|
71
71
|
ModelError::Validation(format!("{}: unterminated front matter", path.display()))
|
|
72
72
|
})?;
|
|
73
|
-
let after_marker = after_meta
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
ModelError::Validation(format!("{}: unterminated front matter", path.display()))
|
|
77
|
-
})?;
|
|
73
|
+
let after_marker = after_meta.strip_prefix("\n---").ok_or_else(|| {
|
|
74
|
+
ModelError::Validation(format!("{}: unterminated front matter", path.display()))
|
|
75
|
+
})?;
|
|
78
76
|
let meta = if raw_meta.trim().is_empty() {
|
|
79
77
|
Value::Map(Vec::new())
|
|
80
78
|
} else {
|
|
@@ -89,7 +87,9 @@ pub fn read_front_matter(path: &Path) -> Result<(Value, String), ModelError> {
|
|
|
89
87
|
Ok((meta, after_marker.trim_start_matches('\n').to_string()))
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
pub fn ignored_owner_team_id_from_team_md(
|
|
90
|
+
pub fn ignored_owner_team_id_from_team_md(
|
|
91
|
+
team_dir: &Path,
|
|
92
|
+
) -> Result<Option<IgnoredTeamField>, ModelError> {
|
|
93
93
|
let team_md = team_dir.join("TEAM.md");
|
|
94
94
|
if !team_md.exists() {
|
|
95
95
|
return Ok(None);
|
|
@@ -138,8 +138,8 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
138
138
|
for entry in fs::read_dir(&agents_dir)
|
|
139
139
|
.map_err(|e| ModelError::Runtime(format!("{}: {e}", agents_dir.display())))?
|
|
140
140
|
{
|
|
141
|
-
let entry =
|
|
142
|
-
.map_err(|e| ModelError::Runtime(format!("{}: {e}", agents_dir.display())))?;
|
|
141
|
+
let entry =
|
|
142
|
+
entry.map_err(|e| ModelError::Runtime(format!("{}: {e}", agents_dir.display())))?;
|
|
143
143
|
let path = entry.path();
|
|
144
144
|
if path.extension().and_then(|s| s.to_str()) == Some("md") {
|
|
145
145
|
role_paths.push(path);
|
|
@@ -156,7 +156,8 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
156
156
|
|
|
157
157
|
let workspace = paths::team_workspace(team_dir)?;
|
|
158
158
|
let workspace_s = workspace.display().to_string();
|
|
159
|
-
let team_name =
|
|
159
|
+
let team_name =
|
|
160
|
+
string_field(&team_meta, "name").unwrap_or_else(|| team_dir_parent_name(team_dir));
|
|
160
161
|
let objective = string_field(&team_meta, "objective")
|
|
161
162
|
.or_else(|| non_empty_trimmed(&team_body))
|
|
162
163
|
.unwrap_or_else(|| "Team Agent document-driven team.".to_string());
|
|
@@ -180,7 +181,10 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
180
181
|
.map(|id| {
|
|
181
182
|
map(vec![
|
|
182
183
|
("id", Value::Str(format!("route-{id}"))),
|
|
183
|
-
(
|
|
184
|
+
(
|
|
185
|
+
"match",
|
|
186
|
+
map(vec![("assignee", list_str(vec![id.as_str()]))]),
|
|
187
|
+
),
|
|
184
188
|
("assign_to", Value::Str(id.clone())),
|
|
185
189
|
("priority", Value::Int(10)),
|
|
186
190
|
])
|
|
@@ -250,9 +254,15 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
250
254
|
map(vec![
|
|
251
255
|
("protocol", Value::Str("mcp_inbox".to_string())),
|
|
252
256
|
("topology", Value::Str("leader_centered".to_string())),
|
|
253
|
-
(
|
|
257
|
+
(
|
|
258
|
+
"worker_to_worker",
|
|
259
|
+
bool_field(&team_meta, "worker_to_worker", true),
|
|
260
|
+
),
|
|
254
261
|
("ack_timeout_sec", Value::Int(60)),
|
|
255
|
-
(
|
|
262
|
+
(
|
|
263
|
+
"result_format",
|
|
264
|
+
Value::Str("result_envelope_v1".to_string()),
|
|
265
|
+
),
|
|
256
266
|
(
|
|
257
267
|
"message_store",
|
|
258
268
|
map(vec![
|
|
@@ -273,19 +283,34 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
273
283
|
.unwrap_or_else(|| "adaptive".to_string()),
|
|
274
284
|
),
|
|
275
285
|
),
|
|
276
|
-
(
|
|
286
|
+
(
|
|
287
|
+
"session_name",
|
|
288
|
+
Value::Str(session_name(&team_meta, &team_name)),
|
|
289
|
+
),
|
|
277
290
|
("auto_launch", Value::Bool(true)),
|
|
278
291
|
("require_user_approval_before_launch", Value::Bool(true)),
|
|
279
|
-
(
|
|
292
|
+
(
|
|
293
|
+
"max_active_agents",
|
|
294
|
+
Value::Int(max_active_agents(agent_ids.len())),
|
|
295
|
+
),
|
|
280
296
|
("startup_order", list_str(agent_ids)),
|
|
281
297
|
(
|
|
282
298
|
"dangerous_auto_approve",
|
|
283
299
|
bool_field(&team_meta, "dangerous_auto_approve", false),
|
|
284
300
|
),
|
|
285
301
|
("fast", bool_field(&team_meta, "fast", false)),
|
|
286
|
-
(
|
|
287
|
-
|
|
288
|
-
|
|
302
|
+
(
|
|
303
|
+
"tick_interval_sec",
|
|
304
|
+
int_field(&team_meta, "tick_interval_sec", 2),
|
|
305
|
+
),
|
|
306
|
+
(
|
|
307
|
+
"push_min_interval_sec",
|
|
308
|
+
int_field(&team_meta, "push_min_interval_sec", 60),
|
|
309
|
+
),
|
|
310
|
+
(
|
|
311
|
+
"stuck_timeout_sec",
|
|
312
|
+
int_field(&team_meta, "stuck_timeout_sec", 300),
|
|
313
|
+
),
|
|
289
314
|
]),
|
|
290
315
|
),
|
|
291
316
|
(
|
|
@@ -310,11 +335,17 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
310
335
|
"tasks",
|
|
311
336
|
Value::List(vec![map(vec![
|
|
312
337
|
("id", Value::Str("task_initial".to_string())),
|
|
313
|
-
(
|
|
338
|
+
(
|
|
339
|
+
"title",
|
|
340
|
+
Value::Str("Initial document-driven team task".to_string()),
|
|
341
|
+
),
|
|
314
342
|
("type", Value::Str("implementation".to_string())),
|
|
315
343
|
("assignee", Value::Str(default_assignee)),
|
|
316
344
|
("deps", Value::List(Vec::new())),
|
|
317
|
-
(
|
|
345
|
+
(
|
|
346
|
+
"acceptance",
|
|
347
|
+
list_str(vec!["Worker reports valid result_envelope_v1"]),
|
|
348
|
+
),
|
|
318
349
|
("status", Value::Str("pending".to_string())),
|
|
319
350
|
("requires_tools", list_str(vec!["mcp_team"])),
|
|
320
351
|
("files", Value::List(Vec::new())),
|
|
@@ -451,7 +482,9 @@ where
|
|
|
451
482
|
}
|
|
452
483
|
|
|
453
484
|
fn string_field(meta: &Value, key: &str) -> Option<String> {
|
|
454
|
-
meta.get(key)
|
|
485
|
+
meta.get(key)
|
|
486
|
+
.and_then(Value::as_str)
|
|
487
|
+
.map(ToString::to_string)
|
|
455
488
|
}
|
|
456
489
|
|
|
457
490
|
fn required_string(meta: &Value, path: &Path, key: &str) -> Result<String, ModelError> {
|
|
@@ -524,8 +557,8 @@ fn resolve_model(role_meta: &Value, team_meta: &Value, provider: &str) -> Value
|
|
|
524
557
|
if let Some(model) = string_field(role_meta, "model") {
|
|
525
558
|
return Value::Str(model);
|
|
526
559
|
}
|
|
527
|
-
if let Some(model) =
|
|
528
|
-
.or_else(|| string_field(team_meta, "default_model"))
|
|
560
|
+
if let Some(model) =
|
|
561
|
+
provider_model(team_meta, provider).or_else(|| string_field(team_meta, "default_model"))
|
|
529
562
|
{
|
|
530
563
|
return Value::Str(model);
|
|
531
564
|
}
|
|
@@ -53,9 +53,9 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
53
53
|
use crate::model::enums::PaneLiveness;
|
|
54
54
|
use crate::transport::{
|
|
55
55
|
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
56
|
-
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName,
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome,
|
|
57
|
+
SpawnResult, SubmitVerification, Target, Transport, TransportError, TurnVerification,
|
|
58
|
+
WindowName,
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
use conpty_transport::{
|
|
@@ -164,8 +164,8 @@ impl ConPtyBackend {
|
|
|
164
164
|
detail: format!("hello failed: {:?}", resp.error),
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
|
-
let hello: HelloResult =
|
|
168
|
-
.map_err(|e| TransportError::MuxUnavailable {
|
|
167
|
+
let hello: HelloResult =
|
|
168
|
+
serde_json::from_value(resp.result).map_err(|e| TransportError::MuxUnavailable {
|
|
169
169
|
backend: BackendKind::ConPty,
|
|
170
170
|
detail: format!("hello response malformed: {e}"),
|
|
171
171
|
})?;
|
|
@@ -303,12 +303,11 @@ impl ConPtyBackend {
|
|
|
303
303
|
source: std::io::Error::other(e),
|
|
304
304
|
})?;
|
|
305
305
|
let resp = self.dispatch(Op::Spawn, payload)?;
|
|
306
|
-
let spawn: ProtoSpawnResult =
|
|
307
|
-
TransportError::Spawn {
|
|
306
|
+
let spawn: ProtoSpawnResult =
|
|
307
|
+
serde_json::from_value(resp.result).map_err(|e| TransportError::Spawn {
|
|
308
308
|
backend: BackendKind::ConPty,
|
|
309
309
|
source: std::io::Error::other(e),
|
|
310
|
-
}
|
|
311
|
-
})?;
|
|
310
|
+
})?;
|
|
312
311
|
Ok(SpawnResult {
|
|
313
312
|
pane_id: PaneId::new(spawn.pane_id),
|
|
314
313
|
session: SessionName::new(spawn.session),
|
|
@@ -368,13 +367,17 @@ fn map_protocol_error(err: Option<&ProtocolError>) -> TransportError {
|
|
|
368
367
|
backend: BackendKind::ConPty,
|
|
369
368
|
detail: format!("pipe_token_mismatch: {message}"),
|
|
370
369
|
},
|
|
371
|
-
ProtocolError::SchemaSkew {
|
|
370
|
+
ProtocolError::SchemaSkew {
|
|
371
|
+
message,
|
|
372
|
+
sent,
|
|
373
|
+
expected,
|
|
374
|
+
} => TransportError::MuxUnavailable {
|
|
372
375
|
backend: BackendKind::ConPty,
|
|
373
376
|
detail: format!("schema_skew sent={sent} expected={expected}: {message}"),
|
|
374
377
|
},
|
|
375
|
-
ProtocolError::TargetNotFound { message } => {
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
+
ProtocolError::TargetNotFound { message } => TransportError::TargetNotFound {
|
|
379
|
+
target: message.clone(),
|
|
380
|
+
},
|
|
378
381
|
ProtocolError::Spawn { message } => TransportError::Spawn {
|
|
379
382
|
backend: BackendKind::ConPty,
|
|
380
383
|
source: std::io::Error::other(message.clone()),
|
|
@@ -556,11 +559,10 @@ impl Transport for ConPtyBackend {
|
|
|
556
559
|
source: std::io::Error::other(e),
|
|
557
560
|
})?;
|
|
558
561
|
let resp = self.dispatch(Op::Capture, payload)?;
|
|
559
|
-
let cap: protocol::CaptureResult =
|
|
560
|
-
TransportError::Capture {
|
|
562
|
+
let cap: protocol::CaptureResult =
|
|
563
|
+
serde_json::from_value(resp.result).map_err(|e| TransportError::Capture {
|
|
561
564
|
source: std::io::Error::other(e),
|
|
562
|
-
}
|
|
563
|
-
})?;
|
|
565
|
+
})?;
|
|
564
566
|
Ok(CapturedText {
|
|
565
567
|
text: cap.text,
|
|
566
568
|
range,
|
|
@@ -572,10 +574,7 @@ impl Transport for ConPtyBackend {
|
|
|
572
574
|
}
|
|
573
575
|
|
|
574
576
|
fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
575
|
-
let resp = self.dispatch(
|
|
576
|
-
Op::Liveness,
|
|
577
|
-
serde_json::json!({"pane_id": pane.as_str()}),
|
|
578
|
-
)?;
|
|
577
|
+
let resp = self.dispatch(Op::Liveness, serde_json::json!({"pane_id": pane.as_str()}))?;
|
|
579
578
|
let known = resp.result["known"].as_bool().unwrap_or(false);
|
|
580
579
|
let alive = resp.result["alive"].as_bool().unwrap_or(false);
|
|
581
580
|
Ok(match (known, alive) {
|
|
@@ -645,10 +644,7 @@ impl Transport for ConPtyBackend {
|
|
|
645
644
|
}
|
|
646
645
|
|
|
647
646
|
fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
|
|
648
|
-
let resp = self.dispatch(
|
|
649
|
-
Op::HasPane,
|
|
650
|
-
serde_json::json!({"pane_id": pane.as_str()}),
|
|
651
|
-
)?;
|
|
647
|
+
let resp = self.dispatch(Op::HasPane, serde_json::json!({"pane_id": pane.as_str()}))?;
|
|
652
648
|
Ok(Some(resp.result["present"].as_bool().unwrap_or(false)))
|
|
653
649
|
}
|
|
654
650
|
|
|
@@ -673,10 +669,7 @@ impl Transport for ConPtyBackend {
|
|
|
673
669
|
fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
|
|
674
670
|
match target {
|
|
675
671
|
Target::Pane(pane) => {
|
|
676
|
-
self.dispatch(
|
|
677
|
-
Op::KillPane,
|
|
678
|
-
serde_json::json!({"pane_id": pane.as_str()}),
|
|
679
|
-
)?;
|
|
672
|
+
self.dispatch(Op::KillPane, serde_json::json!({"pane_id": pane.as_str()}))?;
|
|
680
673
|
}
|
|
681
674
|
Target::SessionWindow { session, window } => {
|
|
682
675
|
self.dispatch(
|
|
@@ -692,10 +685,7 @@ impl Transport for ConPtyBackend {
|
|
|
692
685
|
}
|
|
693
686
|
|
|
694
687
|
fn kill_pane(&self, pane: &PaneId) -> Result<(), TransportError> {
|
|
695
|
-
self.dispatch(
|
|
696
|
-
Op::KillPane,
|
|
697
|
-
serde_json::json!({"pane_id": pane.as_str()}),
|
|
698
|
-
)?;
|
|
688
|
+
self.dispatch(Op::KillPane, serde_json::json!({"pane_id": pane.as_str()}))?;
|
|
699
689
|
Ok(())
|
|
700
690
|
}
|
|
701
691
|
|
|
@@ -350,11 +350,7 @@ fn run_daemon_body_with_panic_marker(
|
|
|
350
350
|
);
|
|
351
351
|
match run_tick_with_panic_marker(&event_log, || coordinator.tick()) {
|
|
352
352
|
Ok(report) => {
|
|
353
|
-
let status = if report.stop {
|
|
354
|
-
"stop_requested"
|
|
355
|
-
} else {
|
|
356
|
-
"ok"
|
|
357
|
-
};
|
|
353
|
+
let status = if report.stop { "stop_requested" } else { "ok" };
|
|
358
354
|
let _ = write_coordinator_heartbeat(
|
|
359
355
|
&args.workspace,
|
|
360
356
|
pid,
|
|
@@ -584,8 +580,7 @@ pub enum DaemonError {
|
|
|
584
580
|
static SIGNAL_STOP_REQUESTED: std::sync::atomic::AtomicBool =
|
|
585
581
|
std::sync::atomic::AtomicBool::new(false);
|
|
586
582
|
#[cfg(unix)]
|
|
587
|
-
static SIGNAL_STOP_NUMBER: std::sync::atomic::AtomicI32 =
|
|
588
|
-
std::sync::atomic::AtomicI32::new(0);
|
|
583
|
+
static SIGNAL_STOP_NUMBER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
|
589
584
|
|
|
590
585
|
#[cfg(unix)]
|
|
591
586
|
extern "C" fn coordinator_signal_handler(signal: libc::c_int) {
|
|
@@ -120,26 +120,31 @@ impl Drop for ShimHandle {
|
|
|
120
120
|
|
|
121
121
|
#[derive(Debug, thiserror::Error)]
|
|
122
122
|
pub enum ShimError {
|
|
123
|
-
#[error(
|
|
124
|
-
|
|
123
|
+
#[error(
|
|
124
|
+
"windows-shim binary not found: expected `{expected}` alongside team-agent.exe; \
|
|
125
|
+
action: reinstall team-agent so the shim exe sits next to the main binary"
|
|
126
|
+
)]
|
|
125
127
|
BinaryMissing { expected: String },
|
|
126
|
-
#[error(
|
|
127
|
-
|
|
128
|
+
#[error(
|
|
129
|
+
"windows-shim spawn failed: {source} (pipe_name={pipe_name}); \
|
|
130
|
+
action: check windows-shim.exe permissions and PATH"
|
|
131
|
+
)]
|
|
128
132
|
Spawn {
|
|
129
133
|
pipe_name: String,
|
|
130
134
|
#[source]
|
|
131
135
|
source: std::io::Error,
|
|
132
136
|
},
|
|
133
|
-
#[error(
|
|
137
|
+
#[error(
|
|
138
|
+
"windows-shim connect timed out after {attempts} attempts (pipe_name={pipe_name}); \
|
|
134
139
|
action: check shim.err.log for CreateNamedPipeW / ACL errors, \
|
|
135
|
-
then re-run team-agent quick-start"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
#[error("windows-shim hello handshake failed: {reason} (pipe_name={pipe_name}); \
|
|
140
|
+
then re-run team-agent quick-start"
|
|
141
|
+
)]
|
|
142
|
+
ConnectTimeout { attempts: u32, pipe_name: String },
|
|
143
|
+
#[error(
|
|
144
|
+
"windows-shim hello handshake failed: {reason} (pipe_name={pipe_name}); \
|
|
141
145
|
action: ensure team-agent.exe and windows-shim.exe are the same build \
|
|
142
|
-
(`sha256sum team-agent.exe windows-shim.exe` matches CI tracking)"
|
|
146
|
+
(`sha256sum team-agent.exe windows-shim.exe` matches CI tracking)"
|
|
147
|
+
)]
|
|
143
148
|
HelloFailed { pipe_name: String, reason: String },
|
|
144
149
|
#[error("state persistence failed after shim spawn: {source}")]
|
|
145
150
|
StatePersist {
|
|
@@ -191,7 +196,11 @@ fn fresh_pipe_token() -> String {
|
|
|
191
196
|
.duration_since(std::time::UNIX_EPOCH)
|
|
192
197
|
.map(|d| d.as_nanos())
|
|
193
198
|
.unwrap_or(0);
|
|
194
|
-
format!(
|
|
199
|
+
format!(
|
|
200
|
+
"{:08x}{:024x}",
|
|
201
|
+
pid,
|
|
202
|
+
nanos & 0xffff_ffff_ffff_ffff_ffff_ffff
|
|
203
|
+
)
|
|
195
204
|
}
|
|
196
205
|
|
|
197
206
|
/// Coordinator hook: spawn `windows-shim.exe` for the given
|
|
@@ -305,10 +314,7 @@ pub fn spawn_shim_and_handshake(
|
|
|
305
314
|
let mut child = child;
|
|
306
315
|
let _ = child.kill();
|
|
307
316
|
let _ = child.wait();
|
|
308
|
-
return Err(ShimError::HelloFailed {
|
|
309
|
-
pipe_name,
|
|
310
|
-
reason,
|
|
311
|
-
});
|
|
317
|
+
return Err(ShimError::HelloFailed { pipe_name, reason });
|
|
312
318
|
}
|
|
313
319
|
}
|
|
314
320
|
}
|
|
@@ -401,12 +407,14 @@ fn finalize(
|
|
|
401
407
|
json!({})
|
|
402
408
|
};
|
|
403
409
|
// CR C-1: token NOT stored. Only pid/pipe_name/pipe_ready.
|
|
404
|
-
let obj = state
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
+
let obj = state
|
|
411
|
+
.as_object_mut()
|
|
412
|
+
.ok_or_else(|| ShimError::StatePersist {
|
|
413
|
+
source: StateError::Io(std::io::Error::new(
|
|
414
|
+
std::io::ErrorKind::InvalidData,
|
|
415
|
+
"state.json root not an object",
|
|
416
|
+
)),
|
|
417
|
+
})?;
|
|
410
418
|
let transport = obj
|
|
411
419
|
.entry("transport".to_string())
|
|
412
420
|
.or_insert_with(|| json!({}));
|
|
@@ -540,12 +548,11 @@ pub fn reconnect_recorded_shim(
|
|
|
540
548
|
team_key: &str,
|
|
541
549
|
_workspace_hash: &str,
|
|
542
550
|
) -> Result<ShimHandle, ShimError> {
|
|
543
|
-
let pipe_name =
|
|
544
|
-
ShimError::ConnectTimeout {
|
|
551
|
+
let pipe_name =
|
|
552
|
+
recorded_shim_pipe_name(workspace).ok_or_else(|| ShimError::ConnectTimeout {
|
|
545
553
|
attempts: 0,
|
|
546
554
|
pipe_name: "<state.transport.shim.pipe_name missing>".to_string(),
|
|
547
|
-
}
|
|
548
|
-
})?;
|
|
555
|
+
})?;
|
|
549
556
|
// For reconnect we don't have the original pipe_token (CR C-1:
|
|
550
557
|
// token was never persisted). Hello is designed to accept any
|
|
551
558
|
// token from the client and echo back the shim's own token, so
|
|
@@ -561,24 +568,19 @@ pub fn reconnect_recorded_shim(
|
|
|
561
568
|
let mut last_err: Option<ShimError> = None;
|
|
562
569
|
for attempt in 1..=CONNECT_ATTEMPTS {
|
|
563
570
|
match NamedPipeClient::connect(&pipe_name, 500) {
|
|
564
|
-
Ok(mut client) => {
|
|
565
|
-
|
|
566
|
-
Ok(
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
Err(reason) => {
|
|
575
|
-
return Err(ShimError::HelloFailed {
|
|
576
|
-
pipe_name,
|
|
577
|
-
reason,
|
|
578
|
-
});
|
|
579
|
-
}
|
|
571
|
+
Ok(mut client) => match reconnect_hello(&mut client, team_key) {
|
|
572
|
+
Ok(()) => {
|
|
573
|
+
return Ok(ShimHandle {
|
|
574
|
+
child: None,
|
|
575
|
+
pid: recorded_shim_pid(workspace).unwrap_or(0),
|
|
576
|
+
pipe_name,
|
|
577
|
+
client: Some(client),
|
|
578
|
+
});
|
|
580
579
|
}
|
|
581
|
-
|
|
580
|
+
Err(reason) => {
|
|
581
|
+
return Err(ShimError::HelloFailed { pipe_name, reason });
|
|
582
|
+
}
|
|
583
|
+
},
|
|
582
584
|
Err(err) => {
|
|
583
585
|
let _ = attempt;
|
|
584
586
|
let _ = placeholder_token;
|
|
@@ -599,10 +601,7 @@ pub fn reconnect_recorded_shim(
|
|
|
599
601
|
/// Reconnect Hello: the client sends Hello with the current workspace/
|
|
600
602
|
/// team scope; the shim replies with its OWN pipe_token (which the
|
|
601
603
|
/// client doesn't know yet). We just validate `resp.ok`.
|
|
602
|
-
fn reconnect_hello(
|
|
603
|
-
client: &mut NamedPipeClient,
|
|
604
|
-
team_key: &str,
|
|
605
|
-
) -> Result<(), String> {
|
|
604
|
+
fn reconnect_hello(client: &mut NamedPipeClient, team_key: &str) -> Result<(), String> {
|
|
606
605
|
use conpty_transport::{Op, PipeClient, Request};
|
|
607
606
|
let req = Request::new(
|
|
608
607
|
"coord-reconnect-hello",
|
|
@@ -633,15 +632,10 @@ fn reconnect_hello(
|
|
|
633
632
|
/// The Windows-only `#[cfg]` gate is at the mod level (see
|
|
634
633
|
/// `coordinator/mod.rs`); on Unix this file isn't compiled, so
|
|
635
634
|
/// downstream callers must cfg-gate their reference themselves.
|
|
636
|
-
pub fn mark_transport_unavailable(
|
|
637
|
-
workspace: &Path,
|
|
638
|
-
reason: &str,
|
|
639
|
-
) -> Result<(), StateError> {
|
|
635
|
+
pub fn mark_transport_unavailable(workspace: &Path, reason: &str) -> Result<(), StateError> {
|
|
640
636
|
// Best-effort event emission — a failed event write should not
|
|
641
637
|
// fail the caller. The state-clearing step below is authoritative.
|
|
642
|
-
if let Ok(event_log) = std::panic::catch_unwind(|| {
|
|
643
|
-
crate::event_log::EventLog::new(workspace)
|
|
644
|
-
}) {
|
|
638
|
+
if let Ok(event_log) = std::panic::catch_unwind(|| crate::event_log::EventLog::new(workspace)) {
|
|
645
639
|
let _ = event_log.write(
|
|
646
640
|
"transport.conpty_shim_unavailable",
|
|
647
641
|
serde_json::json!({
|
|
@@ -659,16 +653,10 @@ pub fn mark_transport_unavailable(
|
|
|
659
653
|
}
|
|
660
654
|
let text = std::fs::read_to_string(&state_path).map_err(StateError::from)?;
|
|
661
655
|
let mut state: Value = serde_json::from_str(&text).unwrap_or_else(|_| serde_json::json!({}));
|
|
662
|
-
if let Some(transport) = state
|
|
663
|
-
.get_mut("transport")
|
|
664
|
-
.and_then(|t| t.as_object_mut())
|
|
665
|
-
{
|
|
656
|
+
if let Some(transport) = state.get_mut("transport").and_then(|t| t.as_object_mut()) {
|
|
666
657
|
if let Some(shim) = transport.get_mut("shim").and_then(|s| s.as_object_mut()) {
|
|
667
658
|
shim.insert("pipe_ready".to_string(), serde_json::json!(false));
|
|
668
|
-
shim.insert(
|
|
669
|
-
"unavailable_reason".to_string(),
|
|
670
|
-
serde_json::json!(reason),
|
|
671
|
-
);
|
|
659
|
+
shim.insert("unavailable_reason".to_string(), serde_json::json!(reason));
|
|
672
660
|
}
|
|
673
661
|
}
|
|
674
662
|
save_runtime_state(workspace, &state)
|
|
@@ -687,9 +675,7 @@ mod tests {
|
|
|
687
675
|
let src = include_str!("conpty_shim.rs");
|
|
688
676
|
// Locate the `finalize` fn body and grep for pipe_token
|
|
689
677
|
// insertion.
|
|
690
|
-
let (_, finalize_and_after) = src
|
|
691
|
-
.split_once("fn finalize(")
|
|
692
|
-
.expect("finalize fn present");
|
|
678
|
+
let (_, finalize_and_after) = src.split_once("fn finalize(").expect("finalize fn present");
|
|
693
679
|
let finalize_body = finalize_and_after
|
|
694
680
|
.split_once("\n}")
|
|
695
681
|
.map(|(body, _)| body)
|
|
@@ -711,7 +697,9 @@ mod tests {
|
|
|
711
697
|
// {pid, pipe_name, pipe_ready}. If a future edit adds a 4th
|
|
712
698
|
// key that carries a secret, this test fires.
|
|
713
699
|
let src = include_str!("conpty_shim.rs");
|
|
714
|
-
let (_, after) = src
|
|
700
|
+
let (_, after) = src
|
|
701
|
+
.split_once("\"shim\".to_string(),")
|
|
702
|
+
.expect("shim insert");
|
|
715
703
|
let block_end = after.find("}),").unwrap_or(after.len());
|
|
716
704
|
let block = &after[..block_end];
|
|
717
705
|
for expected in ["pid", "pipe_name", "pipe_ready"] {
|
|
@@ -178,13 +178,12 @@ pub fn start_coordinator_with_team(
|
|
|
178
178
|
action: None,
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
|
-
let rotation_reason =
|
|
182
|
-
&& !health.metadata_ok
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
};
|
|
181
|
+
let rotation_reason =
|
|
182
|
+
if matches!(health.status, CoordinatorHealthStatus::Running) && !health.metadata_ok {
|
|
183
|
+
health.metadata_mismatch_reason.clone()
|
|
184
|
+
} else {
|
|
185
|
+
None
|
|
186
|
+
};
|
|
188
187
|
if matches!(health.status, CoordinatorHealthStatus::Running) && !health.metadata_ok {
|
|
189
188
|
crate::event_log::EventLog::new(workspace.as_path()).write(
|
|
190
189
|
"coordinator.rotation_required",
|
|
@@ -215,7 +214,10 @@ pub fn start_coordinator_with_team(
|
|
|
215
214
|
binary_identity_relation: health.binary_identity_relation,
|
|
216
215
|
log: None,
|
|
217
216
|
schema_error: None,
|
|
218
|
-
action: Some(
|
|
217
|
+
action: Some(
|
|
218
|
+
"refusing to rotate coordinator metadata that points at the caller process"
|
|
219
|
+
.to_string(),
|
|
220
|
+
),
|
|
219
221
|
});
|
|
220
222
|
}
|
|
221
223
|
match stop_coordinator(workspace) {
|
|
@@ -413,8 +415,7 @@ fn discover_coordinator_pids(workspace: &WorkspacePath) -> Vec<Pid> {
|
|
|
413
415
|
Command::new("ps").args(["-axo", "pid=,command="]),
|
|
414
416
|
"ps_table",
|
|
415
417
|
None,
|
|
416
|
-
)
|
|
417
|
-
{
|
|
418
|
+
) {
|
|
418
419
|
Ok(output) if output.status.success() => output,
|
|
419
420
|
_ => return Vec::new(),
|
|
420
421
|
};
|
|
@@ -432,9 +433,7 @@ fn discover_coordinator_pids(workspace: &WorkspacePath) -> Vec<Pid> {
|
|
|
432
433
|
|
|
433
434
|
fn parse_ps_command_line(line: &str) -> Option<(u32, &str)> {
|
|
434
435
|
let line = line.trim_start();
|
|
435
|
-
let split = line
|
|
436
|
-
.find(char::is_whitespace)
|
|
437
|
-
.unwrap_or(line.len());
|
|
436
|
+
let split = line.find(char::is_whitespace).unwrap_or(line.len());
|
|
438
437
|
let pid = line.get(..split)?.trim().parse::<u32>().ok()?;
|
|
439
438
|
let command = line.get(split..)?.trim();
|
|
440
439
|
Some((pid, command))
|
|
@@ -455,9 +454,13 @@ fn coordinator_command_matches_workspace(command: &str, workspaces: &[String]) -
|
|
|
455
454
|
command
|
|
456
455
|
.split_whitespace()
|
|
457
456
|
.any(|token| token == "team-agent" || token.ends_with("/team-agent"))
|
|
458
|
-
&& command
|
|
457
|
+
&& command
|
|
458
|
+
.split_whitespace()
|
|
459
|
+
.any(|token| token == "coordinator")
|
|
459
460
|
&& command.contains("--workspace")
|
|
460
|
-
&& workspaces
|
|
461
|
+
&& workspaces
|
|
462
|
+
.iter()
|
|
463
|
+
.any(|workspace| command.contains(workspace))
|
|
461
464
|
}
|
|
462
465
|
|
|
463
466
|
fn terminate_pid(pid: Pid) -> bool {
|
|
@@ -505,17 +508,17 @@ fn process_tree_pids(root: Pid) -> Vec<Pid> {
|
|
|
505
508
|
"ps_parent",
|
|
506
509
|
None,
|
|
507
510
|
)
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
511
|
+
.ok()
|
|
512
|
+
.map(|out| String::from_utf8_lossy(&out.stdout).to_string())
|
|
513
|
+
.unwrap_or_default()
|
|
514
|
+
.lines()
|
|
515
|
+
.filter_map(|line| {
|
|
516
|
+
let mut parts = line.split_whitespace();
|
|
517
|
+
let pid = parts.next()?.parse::<u32>().ok()?;
|
|
518
|
+
let ppid = parts.next()?.parse::<u32>().ok()?;
|
|
519
|
+
Some((pid, ppid))
|
|
520
|
+
})
|
|
521
|
+
.collect::<Vec<_>>();
|
|
519
522
|
let mut out = Vec::new();
|
|
520
523
|
collect_child_pids(root_pid, &pairs, &mut out);
|
|
521
524
|
out.push(root_pid);
|
|
@@ -718,11 +721,17 @@ fn coordinator_binary_identity_mismatch_reason(
|
|
|
718
721
|
let Some(metadata) = metadata else {
|
|
719
722
|
return Some(CoordinatorMetadataMismatchReason::MetadataMissing);
|
|
720
723
|
};
|
|
721
|
-
let Some(binary_version) = metadata
|
|
724
|
+
let Some(binary_version) = metadata
|
|
725
|
+
.binary_version
|
|
726
|
+
.as_deref()
|
|
727
|
+
.filter(|value| !value.is_empty())
|
|
722
728
|
else {
|
|
723
729
|
return Some(CoordinatorMetadataMismatchReason::BinaryIdentityMissing);
|
|
724
730
|
};
|
|
725
|
-
let Some(binary_path) = metadata
|
|
731
|
+
let Some(binary_path) = metadata
|
|
732
|
+
.binary_path
|
|
733
|
+
.as_deref()
|
|
734
|
+
.filter(|value| !value.is_empty())
|
|
726
735
|
else {
|
|
727
736
|
return Some(CoordinatorMetadataMismatchReason::BinaryIdentityMissing);
|
|
728
737
|
};
|
|
@@ -928,7 +937,9 @@ fn collect_event_lines(
|
|
|
928
937
|
let archive_signature = file_signature(&archive_path)?;
|
|
929
938
|
let mut lines = Vec::new();
|
|
930
939
|
|
|
931
|
-
let size = std::fs::metadata(&events_path)
|
|
940
|
+
let size = std::fs::metadata(&events_path)
|
|
941
|
+
.map(|m| m.len())
|
|
942
|
+
.unwrap_or(0);
|
|
932
943
|
let rotated = cursor.initialized
|
|
933
944
|
&& (cursor.archive_signature != archive_signature || cursor.event_offset > size);
|
|
934
945
|
if rotated {
|
|
@@ -1004,7 +1015,10 @@ fn collect_result_lines(
|
|
|
1004
1015
|
let mut summary = crate::message_store::result_summary_from_row(&row)
|
|
1005
1016
|
.unwrap_or_else(|| serde_json::json!({}));
|
|
1006
1017
|
if let Some(obj) = summary.as_object_mut() {
|
|
1007
|
-
obj.insert(
|
|
1018
|
+
obj.insert(
|
|
1019
|
+
"event".to_string(),
|
|
1020
|
+
Value::String("result_received".to_string()),
|
|
1021
|
+
);
|
|
1008
1022
|
}
|
|
1009
1023
|
if let Some(rendered) = render_event_line(&summary) {
|
|
1010
1024
|
lines.push(rendered);
|
|
@@ -1100,7 +1114,8 @@ fn file_signature(path: &Path) -> Result<Option<(u64, i128)>, WatchError> {
|
|
|
1100
1114
|
}
|
|
1101
1115
|
|
|
1102
1116
|
fn first_field<'a>(event: &'a Value, keys: &[&str]) -> Option<&'a str> {
|
|
1103
|
-
keys.iter()
|
|
1117
|
+
keys.iter()
|
|
1118
|
+
.find_map(|key| event.get(*key).and_then(Value::as_str))
|
|
1104
1119
|
}
|
|
1105
1120
|
|
|
1106
1121
|
fn clean_field(event: &Value, keys: &[&str], default: &str) -> String {
|