chore(wip): checkpoint P8/C avant chantier Codex inter-agents
Sauvegarde de l'arbre de travail en cours (persistance P8, conversations C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le support de la délégation inter-agents pour les profils Codex. Le round-trip inter-agent question/réponse est couvert sans tokens par les tests loopback existants (state::mcp_e2e_loopback_tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -461,8 +461,8 @@ impl ChangeAgentProfile {
|
||||
mutated_agent = Some(agent);
|
||||
}
|
||||
}
|
||||
let agent = mutated_agent
|
||||
.ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
|
||||
let agent =
|
||||
mutated_agent.ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
|
||||
let manifest = AgentManifest::new(manifest.version, entries)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
self.contexts
|
||||
@ -524,8 +524,10 @@ impl ChangeAgentProfile {
|
||||
// Capture the preserved pair id (stable across all leaves of this
|
||||
// agent) — used to relaunch with handoff re-injection (P7).
|
||||
if pair_id.is_none() {
|
||||
if let Some(cid) =
|
||||
named.tree.leaf(leaf_id).and_then(|l| l.conversation_id.clone())
|
||||
if let Some(cid) = named
|
||||
.tree
|
||||
.leaf(leaf_id)
|
||||
.and_then(|l| l.conversation_id.clone())
|
||||
{
|
||||
pair_id = Some(cid);
|
||||
}
|
||||
@ -1300,14 +1302,13 @@ impl LaunchAgent {
|
||||
// pure partagée avec `resolve_conversation` (aucun couplage à
|
||||
// l'orchestrateur). C'est cet id — et **non** l'id de session moteur —
|
||||
// qui retrouve log + handoff au (re)lancement (P7) et survit au swap.
|
||||
let pair_conversation_id =
|
||||
input.conversation_id.clone().unwrap_or_else(|| {
|
||||
ConversationId::for_pair(
|
||||
ConversationParty::User,
|
||||
ConversationParty::agent(agent.id),
|
||||
)
|
||||
.to_string()
|
||||
});
|
||||
let pair_conversation_id = input.conversation_id.clone().unwrap_or_else(|| {
|
||||
ConversationId::for_pair(
|
||||
ConversationParty::User,
|
||||
ConversationParty::agent(agent.id),
|
||||
)
|
||||
.to_string()
|
||||
});
|
||||
return self
|
||||
.launch_structured(
|
||||
factory.as_ref(),
|
||||
@ -1679,10 +1680,18 @@ impl LaunchAgent {
|
||||
///
|
||||
/// Dispatch by [`McpConfigStrategy`]:
|
||||
/// - `ConfigFile { target }` → write `<run_dir>/<target>` (e.g. `.mcp.json`) with
|
||||
/// the IdeA MCP server declaration. **Non-clobbering and best-effort**, exactly
|
||||
/// like [`Self::seed_cli_permissions`]: an existing file (possibly user-edited)
|
||||
/// is left untouched, and any write/exists failure is swallowed so it **never**
|
||||
/// fails the launch.
|
||||
/// the IdeA MCP server declaration. **Best-effort** (any write/exists failure is
|
||||
/// swallowed so it **never** fails the launch), with two clobber regimes:
|
||||
/// * with an injected [`McpRuntime`] (real app-tauri launch) the file is
|
||||
/// **regenerated and clobbered on every (re)launch**, exactly like the
|
||||
/// convention file. The declaration's `command` carries the **stable** IdeA
|
||||
/// exe path (`$APPIMAGE`) and the project's live endpoint — both drift between
|
||||
/// runs (the AppImage mount path changes at each remount/reboot), so a stale
|
||||
/// `.mcp.json` would point the bridge at a dead binary/socket. `.mcp.json` is
|
||||
/// IdeA-managed (not user-edited), so clobbering is safe;
|
||||
/// * without a runtime (orchestrator / hot-swap / tests) only a degraded minimal
|
||||
/// declaration is available, so the write stays **non-clobbering** (mirrors
|
||||
/// [`Self::seed_cli_permissions`]) — never overwriting a real declaration.
|
||||
/// - `Flag { flag }` → append the flag + run-dir config path to [`SpawnSpec::args`].
|
||||
/// - `Env { var }` → append the variable (run-dir config path) to [`SpawnSpec::env`].
|
||||
///
|
||||
@ -1708,17 +1717,33 @@ impl LaunchAgent {
|
||||
};
|
||||
match &mcp.config {
|
||||
domain::profile::McpConfigStrategy::ConfigFile { target } => {
|
||||
// Non-clobbering, best-effort — mirrors `seed_cli_permissions`.
|
||||
let path = RemotePath::new(join(run_dir, target));
|
||||
match self.fs.exists(&path).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
let declaration = mcp_server_declaration(mcp.transport, runtime);
|
||||
// Best-effort: a write failure must not fail the launch.
|
||||
let declaration = mcp_server_declaration(mcp.transport, runtime);
|
||||
match runtime {
|
||||
// Real launch (app-tauri injected the runtime): the declaration
|
||||
// carries the **stable** IdeA executable path (`$APPIMAGE`, resolved
|
||||
// by `idea_exe_path`) and the project's live loopback endpoint. Both
|
||||
// can drift between runs — the AppImage internal mount path changes
|
||||
// at every remount/reboot — so the file MUST be regenerated and
|
||||
// **clobbered** on every (re)launch, exactly like the convention file
|
||||
// (`apply_injection`). `.mcp.json` is IdeA-managed, not user-edited,
|
||||
// so there is nothing to preserve. Best-effort: a write failure must
|
||||
// never fail the launch.
|
||||
Some(_) => {
|
||||
let _ = self.fs.write(&path, declaration.as_bytes()).await;
|
||||
}
|
||||
// An exists() probe failure is swallowed too (best-effort).
|
||||
Err(_) => {}
|
||||
// Internal launch (orchestrator / hot-swap / tests): we only have a
|
||||
// degraded **minimal** declaration (no endpoint/project/requester).
|
||||
// Stay non-clobbering — mirrors `seed_cli_permissions` — so we never
|
||||
// overwrite a real declaration previously written by an app-tauri
|
||||
// launch with the minimal one.
|
||||
None => match self.fs.exists(&path).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
let _ = self.fs.write(&path, declaration.as_bytes()).await;
|
||||
}
|
||||
Err(_) => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
domain::profile::McpConfigStrategy::Flag { flag } => {
|
||||
@ -1976,6 +2001,7 @@ fn claude_settings_seed(project_root: &str) -> String {
|
||||
]
|
||||
}},
|
||||
"skipDangerousModePermissionPrompt": true,
|
||||
"enabledMcpjsonServers": ["idea"],
|
||||
"sandbox": {{
|
||||
"enabled": false
|
||||
}}
|
||||
@ -2347,11 +2373,7 @@ mod tests {
|
||||
|
||||
// Exact line format: `- [Title](.ideai/memory/slug.md) — hook (type)`.
|
||||
assert!(doc.contains("- [Alpha](.ideai/memory/alpha-note.md) — the first hook (user)"));
|
||||
assert!(
|
||||
doc.contains(
|
||||
"- [Beta](.ideai/memory/beta-note.md) — the second hook (reference)"
|
||||
)
|
||||
);
|
||||
assert!(doc.contains("- [Beta](.ideai/memory/beta-note.md) — the second hook (reference)"));
|
||||
|
||||
// Deterministic order: first entry precedes the second.
|
||||
let alpha_at = doc.find("[Alpha]").unwrap();
|
||||
@ -2533,15 +2555,7 @@ mod tests {
|
||||
MemoryType::Project,
|
||||
)];
|
||||
let h = handoff("Résumé : on a fini l'étape 2.", Some("Livrer le lot P7"));
|
||||
let doc = compose_convention_file(
|
||||
"/root",
|
||||
"",
|
||||
"# Persona",
|
||||
&[],
|
||||
&memory,
|
||||
Some(&h),
|
||||
false,
|
||||
);
|
||||
let doc = compose_convention_file("/root", "", "# Persona", &[], &memory, Some(&h), false);
|
||||
|
||||
assert!(
|
||||
doc.contains("# Reprise de la conversation"),
|
||||
@ -2636,6 +2650,8 @@ mod tests {
|
||||
// Valid JSON.
|
||||
let parsed: serde_json::Value = serde_json::from_str(&json).expect("seed is valid JSON");
|
||||
assert_eq!(parsed["permissions"]["defaultMode"], "bypassPermissions");
|
||||
// IdeA MCP server pre-approved so idea_* tools load without a prompt.
|
||||
assert_eq!(parsed["enabledMcpjsonServers"][0], "idea");
|
||||
assert_eq!(
|
||||
parsed["permissions"]["additionalDirectories"][0],
|
||||
"/home/me/proj"
|
||||
|
||||
Reference in New Issue
Block a user