@team-agent/installer 0.5.52 → 0.5.53
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 +18 -2
- package/crates/team-agent/src/cli/emit.rs +17 -0
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/types.rs +11 -0
- package/crates/team-agent/src/diagnose/orphans.rs +24 -3
- package/crates/team-agent/src/kill_audit.rs +67 -0
- package/crates/team-agent/src/leader/lease.rs +324 -57
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
- package/crates/team-agent/src/leader/rediscover.rs +6 -0
- package/crates/team-agent/src/leader/start.rs +144 -23
- package/crates/team-agent/src/leader/tests/idle.rs +3 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +110 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +8 -2
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
- package/crates/team-agent/src/lifecycle/types.rs +17 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tools.rs +18 -4
- package/crates/team-agent/src/mcp_server/types.rs +3 -0
- package/crates/team-agent/src/mcp_server/wire.rs +30 -7
- package/crates/team-agent/src/messaging/leader_channel.rs +32 -5
- package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
- package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
- package/crates/team-agent/src/provider/adapter.rs +54 -13
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/session/capture.rs +321 -52
- package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
- package/crates/team-agent/src/provider/session/mod.rs +6 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
- package/crates/team-agent/src/provider/session_scan/common.rs +39 -6
- package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
- package/crates/team-agent/src/provider/session_scan.rs +3 -3
- package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
- package/crates/team-agent/src/provider/tests.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +8 -3
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
|
|
3
|
+
use rusqlite::{params, Connection};
|
|
4
|
+
|
|
5
|
+
use crate::provider::{next_session_token, ProviderError, SessionId};
|
|
6
|
+
|
|
7
|
+
const SESSION_TABLES: [&str; 6] = [
|
|
8
|
+
"turns",
|
|
9
|
+
"checkpoints",
|
|
10
|
+
"session_files",
|
|
11
|
+
"session_refs",
|
|
12
|
+
"forge_trajectory_events",
|
|
13
|
+
"search_index",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Clone)]
|
|
17
|
+
pub(crate) struct CopilotForkMaterialization {
|
|
18
|
+
session_id: SessionId,
|
|
19
|
+
home: PathBuf,
|
|
20
|
+
keep: bool,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl CopilotForkMaterialization {
|
|
24
|
+
pub(crate) fn session_id(&self) -> &SessionId {
|
|
25
|
+
&self.session_id
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub(crate) fn home(&self) -> &Path {
|
|
29
|
+
&self.home
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub(crate) fn keep(&mut self) {
|
|
33
|
+
self.keep = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl Drop for CopilotForkMaterialization {
|
|
38
|
+
fn drop(&mut self) {
|
|
39
|
+
if !self.keep {
|
|
40
|
+
let _ = std::fs::remove_dir_all(&self.home);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub(crate) fn copilot_home() -> Result<PathBuf, ProviderError> {
|
|
46
|
+
if let Some(home) = std::env::var_os("COPILOT_HOME").filter(|value| !value.is_empty()) {
|
|
47
|
+
return Ok(PathBuf::from(home));
|
|
48
|
+
}
|
|
49
|
+
std::env::var_os("HOME")
|
|
50
|
+
.map(PathBuf::from)
|
|
51
|
+
.map(|home| home.join(".copilot"))
|
|
52
|
+
.ok_or_else(|| ProviderError::Io("HOME is unset; cannot resolve COPILOT_HOME".to_string()))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub(crate) fn materialize_copilot_fork(
|
|
56
|
+
workspace: &Path,
|
|
57
|
+
agent_id: &str,
|
|
58
|
+
source_session_id: &SessionId,
|
|
59
|
+
) -> Result<CopilotForkMaterialization, ProviderError> {
|
|
60
|
+
let source_home = copilot_home()?;
|
|
61
|
+
let source_dir = source_home
|
|
62
|
+
.join("session-state")
|
|
63
|
+
.join(source_session_id.as_str());
|
|
64
|
+
let source_db = source_home.join("session-store.db");
|
|
65
|
+
if !source_dir.is_dir() || !source_db.is_file() {
|
|
66
|
+
return Err(ProviderError::Io(format!(
|
|
67
|
+
"copilot source backing is incomplete under {}",
|
|
68
|
+
source_home.display()
|
|
69
|
+
)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let session_id = SessionId::new(next_session_token());
|
|
73
|
+
let parent = workspace
|
|
74
|
+
.join(".team/runtime/provider-session-forks/copilot")
|
|
75
|
+
.join(agent_id);
|
|
76
|
+
let home = parent.join(session_id.as_str());
|
|
77
|
+
let temp = parent.join(format!(
|
|
78
|
+
".{}.tmp-{}",
|
|
79
|
+
session_id.as_str(),
|
|
80
|
+
std::process::id()
|
|
81
|
+
));
|
|
82
|
+
if home.exists() || temp.exists() {
|
|
83
|
+
return Err(ProviderError::Io(format!(
|
|
84
|
+
"copilot fork destination already exists: {}",
|
|
85
|
+
home.display()
|
|
86
|
+
)));
|
|
87
|
+
}
|
|
88
|
+
std::fs::create_dir_all(temp.join("session-state"))
|
|
89
|
+
.map_err(|error| ProviderError::Io(error.to_string()))?;
|
|
90
|
+
|
|
91
|
+
let result = (|| {
|
|
92
|
+
let target_session_dir = temp.join("session-state").join(session_id.as_str());
|
|
93
|
+
copy_tree(&source_dir, &target_session_dir)?;
|
|
94
|
+
rewrite_text_tree(
|
|
95
|
+
&target_session_dir,
|
|
96
|
+
source_session_id.as_str(),
|
|
97
|
+
session_id.as_str(),
|
|
98
|
+
)?;
|
|
99
|
+
rewrite_per_session_db(
|
|
100
|
+
&target_session_dir.join("session.db"),
|
|
101
|
+
source_session_id.as_str(),
|
|
102
|
+
session_id.as_str(),
|
|
103
|
+
)?;
|
|
104
|
+
clone_and_rekey_store(
|
|
105
|
+
&source_db,
|
|
106
|
+
&temp.join("session-store.db"),
|
|
107
|
+
source_session_id.as_str(),
|
|
108
|
+
session_id.as_str(),
|
|
109
|
+
)?;
|
|
110
|
+
std::fs::rename(&temp, &home).map_err(|error| ProviderError::Io(error.to_string()))
|
|
111
|
+
})();
|
|
112
|
+
if let Err(error) = result {
|
|
113
|
+
let _ = std::fs::remove_dir_all(&temp);
|
|
114
|
+
return Err(error);
|
|
115
|
+
}
|
|
116
|
+
Ok(CopilotForkMaterialization {
|
|
117
|
+
session_id,
|
|
118
|
+
home,
|
|
119
|
+
keep: false,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn copy_tree(source: &Path, target: &Path) -> Result<(), ProviderError> {
|
|
124
|
+
std::fs::create_dir_all(target).map_err(|error| ProviderError::Io(error.to_string()))?;
|
|
125
|
+
for entry in std::fs::read_dir(source).map_err(|error| ProviderError::Io(error.to_string()))? {
|
|
126
|
+
let entry = entry.map_err(|error| ProviderError::Io(error.to_string()))?;
|
|
127
|
+
let source_path = entry.path();
|
|
128
|
+
let target_path = target.join(entry.file_name());
|
|
129
|
+
if source_path.is_dir() {
|
|
130
|
+
copy_tree(&source_path, &target_path)?;
|
|
131
|
+
} else {
|
|
132
|
+
std::fs::copy(&source_path, &target_path)
|
|
133
|
+
.map_err(|error| ProviderError::Io(error.to_string()))?;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
Ok(())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn rewrite_text_tree(root: &Path, source: &str, target: &str) -> Result<(), ProviderError> {
|
|
140
|
+
for entry in std::fs::read_dir(root).map_err(|error| ProviderError::Io(error.to_string()))? {
|
|
141
|
+
let entry = entry.map_err(|error| ProviderError::Io(error.to_string()))?;
|
|
142
|
+
let path = entry.path();
|
|
143
|
+
if path.is_dir() {
|
|
144
|
+
rewrite_text_tree(&path, source, target)?;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if path.extension().and_then(|ext| ext.to_str()) == Some("db") {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
let Ok(text) = std::fs::read_to_string(&path) else {
|
|
151
|
+
continue;
|
|
152
|
+
};
|
|
153
|
+
if text.contains(source) {
|
|
154
|
+
std::fs::write(&path, text.replace(source, target))
|
|
155
|
+
.map_err(|error| ProviderError::Io(error.to_string()))?;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
Ok(())
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn clone_and_rekey_store(
|
|
162
|
+
source_db: &Path,
|
|
163
|
+
target_db: &Path,
|
|
164
|
+
source: &str,
|
|
165
|
+
target: &str,
|
|
166
|
+
) -> Result<(), ProviderError> {
|
|
167
|
+
let source_conn = Connection::open(source_db).map_err(sql_error)?;
|
|
168
|
+
source_conn
|
|
169
|
+
.execute("VACUUM INTO ?1", [target_db.to_string_lossy().as_ref()])
|
|
170
|
+
.map_err(sql_error)?;
|
|
171
|
+
drop(source_conn);
|
|
172
|
+
|
|
173
|
+
let mut conn = Connection::open(target_db).map_err(sql_error)?;
|
|
174
|
+
conn.pragma_update(None, "foreign_keys", "OFF")
|
|
175
|
+
.map_err(sql_error)?;
|
|
176
|
+
let tx = conn.transaction().map_err(sql_error)?;
|
|
177
|
+
let session_rows = tx
|
|
178
|
+
.execute(
|
|
179
|
+
"update sessions set id = ?1 where id = ?2",
|
|
180
|
+
params![target, source],
|
|
181
|
+
)
|
|
182
|
+
.map_err(sql_error)?;
|
|
183
|
+
if session_rows != 1 {
|
|
184
|
+
return Err(ProviderError::Io(format!(
|
|
185
|
+
"copilot session-store source row count must be 1, got {session_rows}"
|
|
186
|
+
)));
|
|
187
|
+
}
|
|
188
|
+
for table in SESSION_TABLES {
|
|
189
|
+
require_session_column(&tx, table)?;
|
|
190
|
+
tx.execute(
|
|
191
|
+
&format!("update {table} set session_id = ?1 where session_id = ?2"),
|
|
192
|
+
params![target, source],
|
|
193
|
+
)
|
|
194
|
+
.map_err(sql_error)?;
|
|
195
|
+
}
|
|
196
|
+
tx.commit().map_err(sql_error)?;
|
|
197
|
+
verify_rekeyed_store(&conn, source, target)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn require_session_column(conn: &Connection, table: &str) -> Result<(), ProviderError> {
|
|
201
|
+
let mut stmt = conn
|
|
202
|
+
.prepare(&format!("pragma table_info({table})"))
|
|
203
|
+
.map_err(sql_error)?;
|
|
204
|
+
let mut rows = stmt.query([]).map_err(sql_error)?;
|
|
205
|
+
while let Some(row) = rows.next().map_err(sql_error)? {
|
|
206
|
+
let name: String = row.get(1).map_err(sql_error)?;
|
|
207
|
+
if name == "session_id" {
|
|
208
|
+
return Ok(());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
Err(ProviderError::Io(format!(
|
|
212
|
+
"copilot session-store table {table} is missing session_id"
|
|
213
|
+
)))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fn verify_rekeyed_store(
|
|
217
|
+
conn: &Connection,
|
|
218
|
+
source: &str,
|
|
219
|
+
target: &str,
|
|
220
|
+
) -> Result<(), ProviderError> {
|
|
221
|
+
let target_sessions: i64 = conn
|
|
222
|
+
.query_row(
|
|
223
|
+
"select count(*) from sessions where id = ?1",
|
|
224
|
+
[target],
|
|
225
|
+
|row| row.get(0),
|
|
226
|
+
)
|
|
227
|
+
.map_err(sql_error)?;
|
|
228
|
+
let source_sessions: i64 = conn
|
|
229
|
+
.query_row(
|
|
230
|
+
"select count(*) from sessions where id = ?1",
|
|
231
|
+
[source],
|
|
232
|
+
|row| row.get(0),
|
|
233
|
+
)
|
|
234
|
+
.map_err(sql_error)?;
|
|
235
|
+
if target_sessions != 1 || source_sessions != 0 {
|
|
236
|
+
return Err(ProviderError::Io(
|
|
237
|
+
"copilot session-store session id rekey verification failed".to_string(),
|
|
238
|
+
));
|
|
239
|
+
}
|
|
240
|
+
for table in SESSION_TABLES {
|
|
241
|
+
let remaining: i64 = conn
|
|
242
|
+
.query_row(
|
|
243
|
+
&format!("select count(*) from {table} where session_id = ?1"),
|
|
244
|
+
[source],
|
|
245
|
+
|row| row.get(0),
|
|
246
|
+
)
|
|
247
|
+
.map_err(sql_error)?;
|
|
248
|
+
if remaining != 0 {
|
|
249
|
+
return Err(ProviderError::Io(format!(
|
|
250
|
+
"copilot session-store table {table} still references source session"
|
|
251
|
+
)));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
Ok(())
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fn rewrite_per_session_db(path: &Path, source: &str, target: &str) -> Result<(), ProviderError> {
|
|
258
|
+
if !path.is_file() {
|
|
259
|
+
return Ok(());
|
|
260
|
+
}
|
|
261
|
+
let mut conn = Connection::open(path).map_err(sql_error)?;
|
|
262
|
+
let columns: Vec<(String, String)> = {
|
|
263
|
+
let mut tables = conn
|
|
264
|
+
.prepare("select name from sqlite_master where type = 'table'")
|
|
265
|
+
.map_err(sql_error)?;
|
|
266
|
+
let names = tables
|
|
267
|
+
.query_map([], |row| row.get::<_, String>(0))
|
|
268
|
+
.map_err(sql_error)?
|
|
269
|
+
.collect::<Result<Vec<_>, _>>()
|
|
270
|
+
.map_err(sql_error)?;
|
|
271
|
+
let mut columns = Vec::new();
|
|
272
|
+
for table in names {
|
|
273
|
+
let mut stmt = conn
|
|
274
|
+
.prepare(&format!(
|
|
275
|
+
"pragma table_info('{}')",
|
|
276
|
+
table.replace('\'', "''")
|
|
277
|
+
))
|
|
278
|
+
.map_err(sql_error)?;
|
|
279
|
+
let rows = stmt
|
|
280
|
+
.query_map([], |row| row.get::<_, String>(1))
|
|
281
|
+
.map_err(sql_error)?;
|
|
282
|
+
for column in rows {
|
|
283
|
+
let column = column.map_err(sql_error)?;
|
|
284
|
+
if column == "session_id" || column == "recipient_session_id" {
|
|
285
|
+
columns.push((table.clone(), column));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
columns
|
|
290
|
+
};
|
|
291
|
+
let tx = conn.transaction().map_err(sql_error)?;
|
|
292
|
+
for (table, column) in columns {
|
|
293
|
+
let table = table.replace('"', "\"\"");
|
|
294
|
+
let column = column.replace('"', "\"\"");
|
|
295
|
+
tx.execute(
|
|
296
|
+
&format!("update \"{table}\" set \"{column}\" = ?1 where \"{column}\" = ?2"),
|
|
297
|
+
params![target, source],
|
|
298
|
+
)
|
|
299
|
+
.map_err(sql_error)?;
|
|
300
|
+
}
|
|
301
|
+
tx.commit().map_err(sql_error)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fn sql_error(error: rusqlite::Error) -> ProviderError {
|
|
305
|
+
ProviderError::Io(format!("copilot sqlite: {error}"))
|
|
306
|
+
}
|