@team-agent/installer 0.5.83 → 0.5.85

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.
@@ -130,6 +130,12 @@ fn collect_scoped(
130
130
  owner_team_id: Option<&str>,
131
131
  ) -> Result<serde_json::Value, MessagingError> {
132
132
  let paths = collect_paths(workspace)?;
133
+ #[cfg(test)]
134
+ collection_tests::boundary("before_lock")?;
135
+ // Serialize collectors, but do not hold a SQLite write transaction while
136
+ // committing state. A process exit releases this existing OS file lock.
137
+ let collection_lock =
138
+ crate::state::persist::acquire_result_collection_lock(&paths.run_workspace)?;
133
139
  let log = EventLog::new(&paths.run_workspace);
134
140
  let resolved_owner_team_id = match owner_team_id.filter(|team| !team.is_empty()) {
135
141
  Some(team) => Some(resolve_owner_team_for_read(
@@ -159,7 +165,11 @@ fn collect_scoped(
159
165
  let store = MessageStore::open(&paths.run_workspace)?;
160
166
  let conn = crate::db::schema::open_db(store.db_path())?;
161
167
  if let Some(path) = result_file {
168
+ #[cfg(test)]
169
+ collection_tests::boundary("before_ingest")?;
162
170
  ingest_result_file(&conn, path, owner_team_id)?;
171
+ #[cfg(test)]
172
+ collection_tests::boundary("after_ingest")?;
163
173
  }
164
174
  let sql = match owner_team_id {
165
175
  Some(_) => {
@@ -197,7 +207,7 @@ fn collect_scoped(
197
207
  let mut collected_results = Vec::new();
198
208
  let mut invalid_results = Vec::new();
199
209
  let mut fatal_invalid_results = 0usize;
200
- let mut state_dirty = false;
210
+ let mut ready = Vec::new();
201
211
  let mut task_updates = Vec::new();
202
212
  for row in rows {
203
213
  let envelope: serde_json::Value = match serde_json::from_str(&row.envelope) {
@@ -242,25 +252,75 @@ fn collect_scoped(
242
252
  )?;
243
253
  continue;
244
254
  };
245
- match owner_team_id {
246
- Some(team) => {
247
- conn.execute(
248
- "update results set status = 'collected' where result_id = ?1 and owner_team_id = ?2",
249
- params![row.result_id.as_str(), team],
250
- )?;
251
- }
252
- None => {
253
- conn.execute(
254
- "update results set status = 'collected' where result_id = ?1",
255
- params![row.result_id.as_str()],
256
- )?;
257
- }
258
- }
259
255
  if scope == "task" {
260
- mark_task_done(&mut state, &row.task_id, &row.result_id);
261
256
  task_updates.push((row.task_id.clone(), row.result_id.clone()));
262
- state_dirty = true;
263
257
  }
258
+ ready.push((row, envelope, scope));
259
+ }
260
+ // Pending DB rows remain eligible until the complete task delta is durable.
261
+ // Preserve the original created_at/result_id order: its last row per task wins.
262
+ if !task_updates.is_empty() {
263
+ let mut task_winners = Vec::new();
264
+ let mut updated = std::collections::BTreeSet::new();
265
+ for (task_id, result_id) in task_updates.iter().rev() {
266
+ if updated.insert(task_id) {
267
+ task_winners.push((task_id.clone(), result_id.clone()));
268
+ }
269
+ }
270
+ #[cfg(test)]
271
+ collection_tests::boundary("before_state")?;
272
+ state = crate::state::repository::StateRepository::new(&paths.run_workspace).commit(
273
+ crate::state::repository::StateWriteIntent::ResultCollection { owner_team_id },
274
+ |latest| {
275
+ for (task_id, result_id) in &task_winners {
276
+ let already_applied = latest
277
+ .get("tasks")
278
+ .and_then(serde_json::Value::as_array)
279
+ .is_some_and(|tasks| {
280
+ tasks.iter().any(|task| {
281
+ task.get("id").and_then(serde_json::Value::as_str)
282
+ == Some(task_id.as_str())
283
+ && task.get("status").and_then(serde_json::Value::as_str)
284
+ == Some("done")
285
+ && task
286
+ .get("accepted_result_id")
287
+ .and_then(serde_json::Value::as_str)
288
+ == Some(result_id.as_str())
289
+ })
290
+ });
291
+ if !already_applied {
292
+ mark_task_done(latest, task_id, result_id);
293
+ }
294
+ }
295
+ },
296
+ )?;
297
+ for (task_id, result_id) in &task_winners {
298
+ let applied = state
299
+ .get("tasks")
300
+ .and_then(serde_json::Value::as_array)
301
+ .is_some_and(|tasks| {
302
+ tasks.iter().any(|task| {
303
+ task.get("id").and_then(serde_json::Value::as_str) == Some(task_id.as_str())
304
+ && task.get("status").and_then(serde_json::Value::as_str)
305
+ == Some("done")
306
+ && task
307
+ .get("accepted_result_id")
308
+ .and_then(serde_json::Value::as_str)
309
+ == Some(result_id.as_str())
310
+ })
311
+ });
312
+ if !applied {
313
+ return Err(MessagingError::Validation(format!(
314
+ "task result projection missing after commit: task_id={task_id} result_id={result_id}"
315
+ )));
316
+ }
317
+ }
318
+ #[cfg(test)]
319
+ collection_tests::boundary("after_state")?;
320
+ }
321
+ for (row, envelope, scope) in ready {
322
+ #[cfg(test)]
323
+ collection_tests::boundary("before_event")?;
264
324
  log.write(
265
325
  "collect.result",
266
326
  serde_json::json!({
@@ -270,6 +330,29 @@ fn collect_scoped(
270
330
  "scope": scope,
271
331
  }),
272
332
  )?;
333
+ #[cfg(test)]
334
+ collection_tests::boundary("after_event")?;
335
+ #[cfg(test)]
336
+ collection_tests::boundary("before_finalize")?;
337
+ let finalized = match owner_team_id {
338
+ Some(team) => {
339
+ conn.execute(
340
+ "update results set status = 'collected' where result_id = ?1 and owner_team_id = ?2 and status not in ('collected', 'invalid')",
341
+ params![row.result_id.as_str(), team],
342
+ )?
343
+ }
344
+ None => {
345
+ conn.execute(
346
+ "update results set status = 'collected' where result_id = ?1 and status not in ('collected', 'invalid')",
347
+ params![row.result_id.as_str()],
348
+ )?
349
+ }
350
+ };
351
+ #[cfg(test)]
352
+ collection_tests::boundary("after_finalize")?;
353
+ if finalized == 0 {
354
+ continue;
355
+ }
273
356
  collected.push(envelope.clone());
274
357
  let summary = serde_json::json!({
275
358
  "result_id": row.result_id,
@@ -286,17 +369,8 @@ fn collect_scoped(
286
369
  });
287
370
  collected_results.push(summary);
288
371
  }
289
- if state_dirty {
290
- state = crate::state::repository::StateRepository::new(&paths.run_workspace).commit(
291
- crate::state::repository::StateWriteIntent::ResultCollection { owner_team_id },
292
- |latest| {
293
- for (task_id, result_id) in &task_updates {
294
- mark_task_done(latest, task_id, result_id);
295
- }
296
- },
297
- )?;
298
- }
299
372
  let counts = result_counts(&conn, owner_team_id)?;
373
+ drop(collection_lock);
300
374
  // results.py:157 — ensure_coordinator=true runs the REAL ensure step; the
301
375
  // `{ok:false,status:"not_required"}` literal is ONLY the ensure=false branch.
302
376
  let coordinator = if ensure_coordinator {
@@ -1483,6 +1557,9 @@ pub fn collect_results_and_notify_watchers(
1483
1557
  }))
1484
1558
  }
1485
1559
 
1560
+ #[cfg(test)]
1561
+ mod collection_tests;
1562
+
1486
1563
  #[cfg(test)]
1487
1564
  mod tests {
1488
1565
  use super::{format_report_result_notification, report_result};
@@ -57,7 +57,7 @@ pub(crate) fn copilot_base_command(
57
57
 
58
58
  /// Translate `McpConfig.raw` canonical schema (`type`) into the copilot
59
59
  /// `mcp add`/`--additional-mcp-config` expected `transport` field
60
- /// (stdio|http|sse). Only the copilot adapter walks this translation —
60
+ /// (stdio|http|sse). Shared by inline plans and lifecycle config files;
61
61
  /// claude/codex paths leave canonical schema untouched.
62
62
  pub(crate) fn copilot_translate_mcp_config(raw: &serde_json::Value) -> serde_json::Value {
63
63
  let Some(servers) = raw.as_object() else {
@@ -157,7 +157,7 @@ fn errno_name(errno: Option<i32>) -> Option<&'static str> {
157
157
  /// semantic mapping. The Batch 0 non-Unix `not_yet_implemented`
158
158
  /// fallback is now removed (CR C-2 fallback burn-down; grep guard
159
159
  /// `platform_fallback_burndown_batch0.rs` flipped in this batch).
160
- struct RuntimeLock {
160
+ pub(crate) struct RuntimeLock {
161
161
  #[allow(dead_code)]
162
162
  file: std::fs::File,
163
163
  }
@@ -204,6 +204,20 @@ impl Drop for RuntimeLock {
204
204
  }
205
205
  }
206
206
 
207
+ pub(crate) fn acquire_result_collection_lock(
208
+ run_workspace: &Path,
209
+ ) -> Result<RuntimeLock, StateError> {
210
+ RuntimeLock::acquire(run_workspace, "result-collection", 2.0)
211
+ }
212
+
213
+ #[cfg(test)]
214
+ pub(crate) fn acquire_state_save_lock(
215
+ workspace: &Path,
216
+ timeout: f64,
217
+ ) -> Result<RuntimeLock, StateError> {
218
+ RuntimeLock::acquire(workspace, "state-save", timeout)
219
+ }
220
+
207
221
  ///
208
222
  /// `save_runtime_state`(bug-084)。`state` 是 state.json 的内存 Value(插入序保留)。
209
223
  /// 注:Python 在此还调 `_migrate_state_identity`(identity slice 落地后接入;本 slice 不改 state 内容)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.83",
3
+ "version": "0.5.85",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.83",
24
- "@team-agent/cli-darwin-x64": "0.5.83",
25
- "@team-agent/cli-linux-x64": "0.5.83"
23
+ "@team-agent/cli-darwin-arm64": "0.5.85",
24
+ "@team-agent/cli-darwin-x64": "0.5.85",
25
+ "@team-agent/cli-linux-x64": "0.5.85"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",