mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Expose connector candidates from imported sessions (#33414)
## What changed - Correlate MCP server attribution in imported session records with connector metadata, and persist normalized connector names in the session import ledger. - Extend `externalAgentConfig/import/readHistories` with connector candidates, including each connector's display name, imported-session count, and detection source. ## Testing - Cover connector detection through the session import and history-read flow. - Verify candidate aggregation uses the latest import for each source session. GitOrigin-RevId: 7f1f7f596925b2e6efcd17afd4490148e1ac9ff1
This commit is contained in:
1 parent
8530440b91
commit
58e4ed17a0
21 files changed
+587
-38
No files matched your search
+34
@@ -9352,6 +9352,12 @@
|
|||||||
"ExternalAgentConfigImportHistoriesReadResponse": {
|
"ExternalAgentConfigImportHistoriesReadResponse": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"connectors": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/v2/ExternalAgentImportedConnectorCandidate"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/v2/ExternalAgentConfigImportHistory"
|
"$ref": "#/definitions/v2/ExternalAgentConfigImportHistory"
|
||||||
@@ -9360,6 +9366,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"connectors",
|
||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"title": "ExternalAgentConfigImportHistoriesReadResponse",
|
"title": "ExternalAgentConfigImportHistoriesReadResponse",
|
||||||
@@ -9594,6 +9601,33 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ExternalAgentImportedConnectorCandidate": {
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sessionCount": {
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"$ref": "#/definitions/v2/ExternalAgentImportedConnectorSource"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"sessionCount",
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ExternalAgentImportedConnectorSource": {
|
||||||
|
"enum": [
|
||||||
|
"remoteMcpServersConfig"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"FeedbackUploadParams": {
|
"FeedbackUploadParams": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
+34
@@ -5545,6 +5545,12 @@
|
|||||||
"ExternalAgentConfigImportHistoriesReadResponse": {
|
"ExternalAgentConfigImportHistoriesReadResponse": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"connectors": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ExternalAgentImportedConnectorCandidate"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/ExternalAgentConfigImportHistory"
|
"$ref": "#/definitions/ExternalAgentConfigImportHistory"
|
||||||
@@ -5553,6 +5559,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"connectors",
|
||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"title": "ExternalAgentConfigImportHistoriesReadResponse",
|
"title": "ExternalAgentConfigImportHistoriesReadResponse",
|
||||||
@@ -5787,6 +5794,33 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ExternalAgentImportedConnectorCandidate": {
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sessionCount": {
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"$ref": "#/definitions/ExternalAgentImportedConnectorSource"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"sessionCount",
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ExternalAgentImportedConnectorSource": {
|
||||||
|
"enum": [
|
||||||
|
"remoteMcpServersConfig"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"FeedbackUploadParams": {
|
"FeedbackUploadParams": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
Generated
+34
@@ -116,9 +116,42 @@
|
|||||||
"SESSIONS"
|
"SESSIONS"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ExternalAgentImportedConnectorCandidate": {
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sessionCount": {
|
||||||
|
"format": "uint32",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"$ref": "#/definitions/ExternalAgentImportedConnectorSource"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"sessionCount",
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ExternalAgentImportedConnectorSource": {
|
||||||
|
"enum": [
|
||||||
|
"remoteMcpServersConfig"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"connectors": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ExternalAgentImportedConnectorCandidate"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/ExternalAgentConfigImportHistory"
|
"$ref": "#/definitions/ExternalAgentConfigImportHistory"
|
||||||
@@ -127,6 +160,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"connectors",
|
||||||
"data"
|
"data"
|
||||||
],
|
],
|
||||||
"title": "ExternalAgentConfigImportHistoriesReadResponse",
|
"title": "ExternalAgentConfigImportHistoriesReadResponse",
|
||||||
|
|||||||
Generated
+2
-1
@@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { ExternalAgentConfigImportHistory } from "./ExternalAgentConfigImportHistory";
|
import type { ExternalAgentConfigImportHistory } from "./ExternalAgentConfigImportHistory";
|
||||||
|
import type { ExternalAgentImportedConnectorCandidate } from "./ExternalAgentImportedConnectorCandidate";
|
||||||
|
|
||||||
export type ExternalAgentConfigImportHistoriesReadResponse = { data: Array<ExternalAgentConfigImportHistory>, };
|
export type ExternalAgentConfigImportHistoriesReadResponse = { data: Array<ExternalAgentConfigImportHistory>, connectors: Array<ExternalAgentImportedConnectorCandidate>, };
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||||
|
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { ExternalAgentImportedConnectorSource } from "./ExternalAgentImportedConnectorSource";
|
||||||
|
|
||||||
|
export type ExternalAgentImportedConnectorCandidate = { name: string, sessionCount: number, source: ExternalAgentImportedConnectorSource, };
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||||
|
|
||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type ExternalAgentImportedConnectorSource = "remoteMcpServersConfig";
|
||||||
@@ -121,6 +121,8 @@ export type { ExternalAgentConfigImportResponse } from "./ExternalAgentConfigImp
|
|||||||
export type { ExternalAgentConfigImportTypeResult } from "./ExternalAgentConfigImportTypeResult";
|
export type { ExternalAgentConfigImportTypeResult } from "./ExternalAgentConfigImportTypeResult";
|
||||||
export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem";
|
export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem";
|
||||||
export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType";
|
export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType";
|
||||||
|
export type { ExternalAgentImportedConnectorCandidate } from "./ExternalAgentImportedConnectorCandidate";
|
||||||
|
export type { ExternalAgentImportedConnectorSource } from "./ExternalAgentImportedConnectorSource";
|
||||||
export type { FeedbackUploadParams } from "./FeedbackUploadParams";
|
export type { FeedbackUploadParams } from "./FeedbackUploadParams";
|
||||||
export type { FeedbackUploadResponse } from "./FeedbackUploadResponse";
|
export type { FeedbackUploadResponse } from "./FeedbackUploadResponse";
|
||||||
export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision";
|
export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision";
|
||||||
|
|||||||
@@ -756,6 +756,23 @@ pub struct ExternalAgentConfigImportHistory {
|
|||||||
#[ts(export_to = "v2/")]
|
#[ts(export_to = "v2/")]
|
||||||
pub struct ExternalAgentConfigImportHistoriesReadResponse {
|
pub struct ExternalAgentConfigImportHistoriesReadResponse {
|
||||||
pub data: Vec<ExternalAgentConfigImportHistory>,
|
pub data: Vec<ExternalAgentConfigImportHistory>,
|
||||||
|
pub connectors: Vec<ExternalAgentImportedConnectorCandidate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum ExternalAgentImportedConnectorSource {
|
||||||
|
RemoteMcpServersConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub struct ExternalAgentImportedConnectorCandidate {
|
||||||
|
pub name: String,
|
||||||
|
pub session_count: u32,
|
||||||
|
pub source: ExternalAgentImportedConnectorSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ Example with notification opt-out:
|
|||||||
- `config/read` — fetch the effective config on disk after resolving config layering, including opaque `desktop` values stored in `config.toml`.
|
- `config/read` — fetch the effective config on disk after resolving config layering, including opaque `desktop` values stored in `config.toml`.
|
||||||
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome`, optional `cwds`, and an optional `source` identifier reserved for adapter selection; omitted, `null`, or unrecognized values retain the default behavior. Each detected item includes `cwd` (`null` for home), and multi-item migrations may additionally include structured `details` with plugin ids, skill names, session metadata, or other artifact names.
|
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome`, optional `cwds`, and an optional `source` identifier reserved for adapter selection; omitted, `null`, or unrecognized values retain the default behavior. Each detected item includes `cwd` (`null` for home), and multi-item migrations may additionally include structured `details` with plugin ids, skill names, session metadata, or other artifact names.
|
||||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Callers may pass `source` to identify the product that initiated the import; omitted or `null` means unspecified. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately.
|
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Callers may pass `source` to identify the product that initiated the import; omitted or `null` means unspecified. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately.
|
||||||
|
- `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Connector candidates include a normalized display `name`, the number of imported sessions that used the connector, and the source metadata field used for detection.
|
||||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.
|
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.
|
||||||
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits.
|
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits.
|
||||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ use codex_app_server_protocol::ExternalAgentConfigImportResponse;
|
|||||||
use codex_app_server_protocol::ExternalAgentConfigImportTypeResult as ProtocolImportTypeResult;
|
use codex_app_server_protocol::ExternalAgentConfigImportTypeResult as ProtocolImportTypeResult;
|
||||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
|
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
|
||||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
|
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
|
||||||
|
use codex_app_server_protocol::ExternalAgentImportedConnectorCandidate;
|
||||||
|
use codex_app_server_protocol::ExternalAgentImportedConnectorSource;
|
||||||
use codex_app_server_protocol::HookMigration;
|
use codex_app_server_protocol::HookMigration;
|
||||||
use codex_app_server_protocol::JSONRPCErrorError;
|
use codex_app_server_protocol::JSONRPCErrorError;
|
||||||
use codex_app_server_protocol::McpServerMigration;
|
use codex_app_server_protocol::McpServerMigration;
|
||||||
@@ -32,6 +34,7 @@ use codex_app_server_protocol::SkillMigration;
|
|||||||
use codex_arg0::Arg0DispatchPaths;
|
use codex_arg0::Arg0DispatchPaths;
|
||||||
use codex_core::ThreadManager;
|
use codex_core::ThreadManager;
|
||||||
use codex_external_agent_migration::sessions::ExternalAgentSessionMigration as CoreSessionMigration;
|
use codex_external_agent_migration::sessions::ExternalAgentSessionMigration as CoreSessionMigration;
|
||||||
|
use codex_external_agent_migration::sessions::read_imported_connector_candidates;
|
||||||
use codex_rollout::StateDbHandle;
|
use codex_rollout::StateDbHandle;
|
||||||
use codex_state::ExternalAgentConfigImportFailureRecord;
|
use codex_state::ExternalAgentConfigImportFailureRecord;
|
||||||
use codex_state::ExternalAgentConfigImportSuccessRecord;
|
use codex_state::ExternalAgentConfigImportSuccessRecord;
|
||||||
@@ -89,15 +92,16 @@ impl ExternalAgentConfigRequestProcessor {
|
|||||||
arg0_paths,
|
arg0_paths,
|
||||||
codex_home,
|
codex_home,
|
||||||
} = args;
|
} = args;
|
||||||
|
let migration_service =
|
||||||
|
ExternalAgentConfigService::new(codex_home.clone(), analytics_events_client.clone());
|
||||||
let session_importer = ExternalAgentSessionImporter::new(
|
let session_importer = ExternalAgentSessionImporter::new(
|
||||||
codex_home.clone(),
|
codex_home,
|
||||||
|
migration_service.connector_metadata_roots.clone(),
|
||||||
Arc::clone(&thread_manager),
|
Arc::clone(&thread_manager),
|
||||||
thread_store,
|
thread_store,
|
||||||
config_manager,
|
config_manager,
|
||||||
arg0_paths,
|
arg0_paths,
|
||||||
);
|
);
|
||||||
let migration_service =
|
|
||||||
ExternalAgentConfigService::new(codex_home, analytics_events_client.clone());
|
|
||||||
Self {
|
Self {
|
||||||
outgoing,
|
outgoing,
|
||||||
migration_service,
|
migration_service,
|
||||||
@@ -369,8 +373,21 @@ impl ExternalAgentConfigRequestProcessor {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(protocol_import_history)
|
.map(protocol_import_history)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let connectors = read_imported_connector_candidates(&self.migration_service.codex_home)
|
||||||
|
.map_err(|err| {
|
||||||
|
internal_error(format!(
|
||||||
|
"failed to read imported connector candidates: {err}"
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.into_iter()
|
||||||
|
.map(|candidate| ExternalAgentImportedConnectorCandidate {
|
||||||
|
name: candidate.name,
|
||||||
|
session_count: candidate.session_count,
|
||||||
|
source: ExternalAgentImportedConnectorSource::RemoteMcpServersConfig,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(ExternalAgentConfigImportHistoriesReadResponse { data })
|
Ok(ExternalAgentConfigImportHistoriesReadResponse { data, connectors })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_pending_session_imports(
|
fn validate_pending_session_imports(
|
||||||
|
|||||||
@@ -187,7 +187,8 @@ pub(crate) struct ExternalAgentConfigMigrationItem {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct ExternalAgentConfigService {
|
pub(crate) struct ExternalAgentConfigService {
|
||||||
codex_home: PathBuf,
|
pub(super) codex_home: PathBuf,
|
||||||
|
pub(super) connector_metadata_roots: Vec<PathBuf>,
|
||||||
external_agent_home: PathBuf,
|
external_agent_home: PathBuf,
|
||||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||||
source: ExternalAgentSource,
|
source: ExternalAgentSource,
|
||||||
@@ -197,8 +198,10 @@ impl ExternalAgentConfigService {
|
|||||||
pub(crate) fn new(codex_home: PathBuf, analytics_events_client: AnalyticsEventsClient) -> Self {
|
pub(crate) fn new(codex_home: PathBuf, analytics_events_client: AnalyticsEventsClient) -> Self {
|
||||||
let source = ExternalAgentSource::default();
|
let source = ExternalAgentSource::default();
|
||||||
let external_agent_home = default_external_agent_home(source);
|
let external_agent_home = default_external_agent_home(source);
|
||||||
|
let connector_metadata_roots = source.connector_metadata_roots(&external_agent_home);
|
||||||
Self {
|
Self {
|
||||||
codex_home,
|
codex_home,
|
||||||
|
connector_metadata_roots,
|
||||||
external_agent_home,
|
external_agent_home,
|
||||||
analytics_events_client: Some(analytics_events_client),
|
analytics_events_client: Some(analytics_events_client),
|
||||||
source,
|
source,
|
||||||
@@ -207,11 +210,14 @@ impl ExternalAgentConfigService {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn new_for_test(codex_home: PathBuf, external_agent_home: PathBuf) -> Self {
|
fn new_for_test(codex_home: PathBuf, external_agent_home: PathBuf) -> Self {
|
||||||
|
let source = ExternalAgentSource::default();
|
||||||
|
let connector_metadata_roots = source.connector_metadata_roots(&external_agent_home);
|
||||||
Self {
|
Self {
|
||||||
codex_home,
|
codex_home,
|
||||||
|
connector_metadata_roots,
|
||||||
external_agent_home,
|
external_agent_home,
|
||||||
analytics_events_client: None,
|
analytics_events_client: None,
|
||||||
source: ExternalAgentSource::default(),
|
source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,12 @@ impl ExternalAgentSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn connector_metadata_roots(self, external_agent_home: &Path) -> Vec<PathBuf> {
|
||||||
|
match self {
|
||||||
|
Self::Cla => source_cla::connector_metadata_roots(external_agent_home),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn marketplace_import_sources(
|
pub(super) fn marketplace_import_sources(
|
||||||
self,
|
self,
|
||||||
external_agent_home: &Path,
|
external_agent_home: &Path,
|
||||||
|
|||||||
@@ -52,6 +52,48 @@ const COMMAND_MIGRATION_PROFILE: CommandMigrationProfile = CommandMigrationProfi
|
|||||||
CommandDescriptionMode::RequireFrontmatter,
|
CommandDescriptionMode::RequireFrontmatter,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
pub(super) fn connector_metadata_roots(external_agent_home: &Path) -> Vec<PathBuf> {
|
||||||
|
let Some(home) = external_agent_home
|
||||||
|
.parent()
|
||||||
|
.filter(|path| !path.as_os_str().is_empty())
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
vec![home.join("Library/Application Support/Claude")]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let default_roaming = home.join("AppData/Roaming");
|
||||||
|
let default_local = home.join("AppData/Local");
|
||||||
|
let roaming = std::env::var_os("APPDATA")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.filter(|path| path.is_absolute())
|
||||||
|
.unwrap_or_else(|| default_roaming.clone());
|
||||||
|
let local = std::env::var_os("LOCALAPPDATA")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.filter(|path| path.is_absolute())
|
||||||
|
.unwrap_or_else(|| default_local.clone());
|
||||||
|
let mut roots = vec![
|
||||||
|
local.join("Packages/Claude_pzs8sxrjxfjjc/LocalCache/Roaming/Claude"),
|
||||||
|
roaming.join("Claude"),
|
||||||
|
default_local.join("Packages/Claude_pzs8sxrjxfjjc/LocalCache/Roaming/Claude"),
|
||||||
|
default_roaming.join("Claude"),
|
||||||
|
];
|
||||||
|
roots.sort();
|
||||||
|
roots.dedup();
|
||||||
|
roots
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
|
{
|
||||||
|
vec![home.join(".config/Claude")]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn effective_settings(project_settings: &Path) -> io::Result<Option<JsonValue>> {
|
pub(super) fn effective_settings(project_settings: &Path) -> io::Result<Option<JsonValue>> {
|
||||||
let mut effective = super::read_external_settings(project_settings)?;
|
let mut effective = super::read_external_settings(project_settings)?;
|
||||||
let Some(settings_dir) = project_settings.parent() else {
|
let Some(settings_dir) = project_settings.parent() else {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ use codex_core::config::ConfigOverrides;
|
|||||||
use codex_external_agent_migration::sessions::CompletedExternalAgentSessionImport;
|
use codex_external_agent_migration::sessions::CompletedExternalAgentSessionImport;
|
||||||
use codex_external_agent_migration::sessions::ExternalAgentSessionMigration;
|
use codex_external_agent_migration::sessions::ExternalAgentSessionMigration;
|
||||||
use codex_external_agent_migration::sessions::ImportedExternalAgentSession;
|
use codex_external_agent_migration::sessions::ImportedExternalAgentSession;
|
||||||
|
use codex_external_agent_migration::sessions::ImportedSessionConnectorAttribution;
|
||||||
use codex_external_agent_migration::sessions::PendingSessionImport;
|
use codex_external_agent_migration::sessions::PendingSessionImport;
|
||||||
|
use codex_external_agent_migration::sessions::detect_imported_cla_session_connectors;
|
||||||
use codex_external_agent_migration::sessions::prepare_validated_session_import;
|
use codex_external_agent_migration::sessions::prepare_validated_session_import;
|
||||||
use codex_external_agent_migration::sessions::record_completed_session_imports;
|
use codex_external_agent_migration::sessions::record_completed_session_imports;
|
||||||
use codex_models_manager::manager::RefreshStrategy;
|
use codex_models_manager::manager::RefreshStrategy;
|
||||||
@@ -33,9 +35,15 @@ use crate::config_manager::ConfigManager;
|
|||||||
|
|
||||||
const SESSION_IMPORT_CONCURRENCY: usize = 5;
|
const SESSION_IMPORT_CONCURRENCY: usize = 5;
|
||||||
|
|
||||||
|
struct CompletedSessionImport {
|
||||||
|
import: CompletedExternalAgentSessionImport,
|
||||||
|
connector_attribution: Option<ImportedSessionConnectorAttribution>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(super) struct ExternalAgentSessionImporter {
|
pub(super) struct ExternalAgentSessionImporter {
|
||||||
codex_home: PathBuf,
|
codex_home: PathBuf,
|
||||||
|
connector_metadata_roots: Vec<PathBuf>,
|
||||||
permits: Arc<Semaphore>,
|
permits: Arc<Semaphore>,
|
||||||
thread_manager: Arc<ThreadManager>,
|
thread_manager: Arc<ThreadManager>,
|
||||||
thread_store: Arc<dyn ThreadStore>,
|
thread_store: Arc<dyn ThreadStore>,
|
||||||
@@ -46,6 +54,7 @@ pub(super) struct ExternalAgentSessionImporter {
|
|||||||
impl ExternalAgentSessionImporter {
|
impl ExternalAgentSessionImporter {
|
||||||
pub(super) fn new(
|
pub(super) fn new(
|
||||||
codex_home: PathBuf,
|
codex_home: PathBuf,
|
||||||
|
connector_metadata_roots: Vec<PathBuf>,
|
||||||
thread_manager: Arc<ThreadManager>,
|
thread_manager: Arc<ThreadManager>,
|
||||||
thread_store: Arc<dyn ThreadStore>,
|
thread_store: Arc<dyn ThreadStore>,
|
||||||
config_manager: ConfigManager,
|
config_manager: ConfigManager,
|
||||||
@@ -53,6 +62,7 @@ impl ExternalAgentSessionImporter {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
codex_home,
|
codex_home,
|
||||||
|
connector_metadata_roots,
|
||||||
permits: Arc::new(Semaphore::new(1)),
|
permits: Arc::new(Semaphore::new(1)),
|
||||||
thread_manager,
|
thread_manager,
|
||||||
thread_store,
|
thread_store,
|
||||||
@@ -91,8 +101,8 @@ impl ExternalAgentSessionImporter {
|
|||||||
match result {
|
match result {
|
||||||
Ok(Some(completed_import)) => {
|
Ok(Some(completed_import)) => {
|
||||||
item_result.record_success(
|
item_result.record_success(
|
||||||
Some(completed_import.source_path.display().to_string()),
|
Some(completed_import.import.source_path.display().to_string()),
|
||||||
Some(completed_import.imported_thread_id.to_string()),
|
Some(completed_import.import.imported_thread_id.to_string()),
|
||||||
);
|
);
|
||||||
completed_imports.push(completed_import);
|
completed_imports.push(completed_import);
|
||||||
}
|
}
|
||||||
@@ -107,6 +117,42 @@ impl ExternalAgentSessionImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let connector_attributions = completed_imports
|
||||||
|
.iter()
|
||||||
|
.filter_map(|completed_import| completed_import.connector_attribution.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let connector_metadata_roots = self.connector_metadata_roots.clone();
|
||||||
|
let mut connector_names_by_session = match tokio::task::spawn_blocking(move || {
|
||||||
|
detect_imported_cla_session_connectors(
|
||||||
|
&connector_attributions,
|
||||||
|
&connector_metadata_roots,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(connector_names_by_session) => connector_names_by_session,
|
||||||
|
Err(err) => {
|
||||||
|
record_import_error(
|
||||||
|
&mut item_result,
|
||||||
|
"session_connector_detection_task",
|
||||||
|
err.to_string(),
|
||||||
|
/*source*/ None,
|
||||||
|
);
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for completed_import in &mut completed_imports {
|
||||||
|
let Some(attribution) = &completed_import.connector_attribution else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
completed_import.import.connector_names = connector_names_by_session
|
||||||
|
.remove(&attribution.session_id)
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
let completed_imports = completed_imports
|
||||||
|
.into_iter()
|
||||||
|
.map(|completed_import| completed_import.import)
|
||||||
|
.collect();
|
||||||
if let Err(err) = record_completed_session_imports(&self.codex_home, completed_imports) {
|
if let Err(err) = record_completed_session_imports(&self.codex_home, completed_imports) {
|
||||||
record_import_error(
|
record_import_error(
|
||||||
&mut item_result,
|
&mut item_result,
|
||||||
@@ -121,7 +167,7 @@ impl ExternalAgentSessionImporter {
|
|||||||
async fn import_requested_session(
|
async fn import_requested_session(
|
||||||
&self,
|
&self,
|
||||||
session: ExternalAgentSessionMigration,
|
session: ExternalAgentSessionMigration,
|
||||||
) -> Result<Option<CompletedExternalAgentSessionImport>, SessionImportFailure> {
|
) -> Result<Option<CompletedSessionImport>, SessionImportFailure> {
|
||||||
let source_path = session.path.clone();
|
let source_path = session.path.clone();
|
||||||
let Some(pending_import) =
|
let Some(pending_import) =
|
||||||
self.prepare_session_import(session)
|
self.prepare_session_import(session)
|
||||||
@@ -134,6 +180,16 @@ impl ExternalAgentSessionImporter {
|
|||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
let connector_attribution = pending_import
|
||||||
|
.source_path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|stem| stem.to_str())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|session_id| !session_id.is_empty())
|
||||||
|
.map(|session_id| ImportedSessionConnectorAttribution {
|
||||||
|
session_id: session_id.to_string(),
|
||||||
|
server_ids: pending_import.attributed_mcp_server_ids,
|
||||||
|
});
|
||||||
let imported_thread_id =
|
let imported_thread_id =
|
||||||
self.persist_session(pending_import.session)
|
self.persist_session(pending_import.session)
|
||||||
.await
|
.await
|
||||||
@@ -142,10 +198,14 @@ impl ExternalAgentSessionImporter {
|
|||||||
message,
|
message,
|
||||||
stage: "session_persist",
|
stage: "session_persist",
|
||||||
})?;
|
})?;
|
||||||
Ok(Some(CompletedExternalAgentSessionImport {
|
Ok(Some(CompletedSessionImport {
|
||||||
source_path: pending_import.source_path,
|
import: CompletedExternalAgentSessionImport {
|
||||||
source_content_sha256: pending_import.source_content_sha256,
|
source_path: pending_import.source_path,
|
||||||
imported_thread_id,
|
source_content_sha256: pending_import.source_content_sha256,
|
||||||
|
imported_thread_id,
|
||||||
|
connector_names: Vec::new(),
|
||||||
|
},
|
||||||
|
connector_attribution,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ use codex_app_server_protocol::ExternalAgentConfigImportHistoriesReadResponse;
|
|||||||
use codex_app_server_protocol::ExternalAgentConfigImportProgressNotification;
|
use codex_app_server_protocol::ExternalAgentConfigImportProgressNotification;
|
||||||
use codex_app_server_protocol::ExternalAgentConfigImportResponse;
|
use codex_app_server_protocol::ExternalAgentConfigImportResponse;
|
||||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
|
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
|
||||||
|
use codex_app_server_protocol::ExternalAgentImportedConnectorCandidate;
|
||||||
|
use codex_app_server_protocol::ExternalAgentImportedConnectorSource;
|
||||||
use codex_app_server_protocol::JSONRPCResponse;
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
use codex_app_server_protocol::PluginListParams;
|
use codex_app_server_protocol::PluginListParams;
|
||||||
use codex_app_server_protocol::PluginListResponse;
|
use codex_app_server_protocol::PluginListResponse;
|
||||||
@@ -46,6 +48,21 @@ fn external_agent_home(codex_home: &Path) -> PathBuf {
|
|||||||
codex_home.join(concat!(".", "cla", "ude"))
|
codex_home.join(concat!(".", "cla", "ude"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn connector_metadata_root(home: &Path) -> PathBuf {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
home.join("Library/Application Support/Claude")
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
home.join("AppData/Roaming/Claude")
|
||||||
|
}
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
|
{
|
||||||
|
home.join(".config/Claude")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_import_response(response: ExternalAgentConfigImportResponse) -> String {
|
fn assert_import_response(response: ExternalAgentConfigImportResponse) -> String {
|
||||||
assert!(!response.import_id.is_empty());
|
assert!(!response.import_id.is_empty());
|
||||||
response.import_id
|
response.import_id
|
||||||
@@ -196,6 +213,7 @@ async fn external_agent_config_import_sends_completion_notification_for_sync_onl
|
|||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
let response: ExternalAgentConfigImportHistoriesReadResponse = to_response(response)?;
|
let response: ExternalAgentConfigImportHistoriesReadResponse = to_response(response)?;
|
||||||
|
assert_eq!(response.connectors, Vec::new());
|
||||||
let entry = response
|
let entry = response
|
||||||
.data
|
.data
|
||||||
.iter()
|
.iter()
|
||||||
@@ -749,10 +767,24 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
|||||||
let recent_timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
let recent_timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||||
let session_dir = external_agent_home(codex_home.path()).join("projects/repo");
|
let session_dir = external_agent_home(codex_home.path()).join("projects/repo");
|
||||||
let session_path = session_dir.join("session.jsonl");
|
let session_path = session_dir.join("session.jsonl");
|
||||||
|
let manifest_dir = connector_metadata_root(codex_home.path())
|
||||||
|
.join("claude-code-sessions/account/organization");
|
||||||
let control_request = "<ide_selection>src/auth.rs:1-5</ide_selection>";
|
let control_request = "<ide_selection>src/auth.rs:1-5</ide_selection>";
|
||||||
let first_request = "Fix auth flow";
|
let first_request = "Fix auth flow";
|
||||||
std::fs::create_dir_all(&project_root)?;
|
std::fs::create_dir_all(&project_root)?;
|
||||||
std::fs::create_dir_all(&session_dir)?;
|
std::fs::create_dir_all(&session_dir)?;
|
||||||
|
std::fs::create_dir_all(&manifest_dir)?;
|
||||||
|
std::fs::write(
|
||||||
|
manifest_dir.join("session.json"),
|
||||||
|
serde_json::json!({
|
||||||
|
"cliSessionId": "session",
|
||||||
|
"remoteMcpServersConfig": [
|
||||||
|
{ "name": "Gmail", "uuid": "gmail-server" },
|
||||||
|
{ "name": "Slack", "uuid": "slack-server" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
)?;
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
&session_path,
|
&session_path,
|
||||||
[
|
[
|
||||||
@@ -774,6 +806,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
|||||||
"type": "assistant",
|
"type": "assistant",
|
||||||
"cwd": &project_root,
|
"cwd": &project_root,
|
||||||
"timestamp": &recent_timestamp,
|
"timestamp": &recent_timestamp,
|
||||||
|
"attributionMcpServer": "gmail-server",
|
||||||
"message": { "content": "first answer" },
|
"message": { "content": "first answer" },
|
||||||
})
|
})
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -861,6 +894,27 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
|||||||
.expect("session success should include imported thread id")
|
.expect("session success should include imported thread id")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
let request_id = mcp
|
||||||
|
.send_raw_request(
|
||||||
|
"externalAgentConfig/import/readHistories",
|
||||||
|
/*params*/ None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let response: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let response: ExternalAgentConfigImportHistoriesReadResponse = to_response(response)?;
|
||||||
|
assert_eq!(
|
||||||
|
response.connectors,
|
||||||
|
vec![ExternalAgentImportedConnectorCandidate {
|
||||||
|
name: "Gmail".to_string(),
|
||||||
|
session_count: 1,
|
||||||
|
source: ExternalAgentImportedConnectorSource::RemoteMcpServersConfig,
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
cursor: None,
|
cursor: None,
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
const SESSION_MANIFESTS_DIR: &str = "claude-code-sessions";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ImportedSessionConnectorAttribution {
|
||||||
|
pub session_id: String,
|
||||||
|
pub server_ids: BTreeSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SessionManifest {
|
||||||
|
cli_session_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
remote_mcp_servers_config: Vec<RemoteMcpServerConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RemoteMcpServerConfig {
|
||||||
|
name: Option<String>,
|
||||||
|
uuid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_imported_cla_session_connectors(
|
||||||
|
session_attributions: &[ImportedSessionConnectorAttribution],
|
||||||
|
connector_metadata_roots: &[PathBuf],
|
||||||
|
) -> BTreeMap<String, Vec<String>> {
|
||||||
|
if session_attributions.is_empty() {
|
||||||
|
return BTreeMap::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributed_server_ids_by_session = session_attributions
|
||||||
|
.iter()
|
||||||
|
.map(|attribution| {
|
||||||
|
(
|
||||||
|
attribution.session_id.clone(),
|
||||||
|
attribution.server_ids.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
let mut connector_names_by_session = BTreeMap::<String, BTreeMap<String, String>>::new();
|
||||||
|
|
||||||
|
for metadata_root in connector_metadata_roots {
|
||||||
|
let manifests_root = metadata_root.join(SESSION_MANIFESTS_DIR);
|
||||||
|
for manifest_path in json_files_recursively(&manifests_root) {
|
||||||
|
let Some(manifest) = read_session_manifest(&manifest_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(session_id) = manifest.cli_session_id else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(attributed_server_ids) = attributed_server_ids_by_session.get(&session_id)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if attributed_server_ids.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connector_names = connector_names_by_session.entry(session_id).or_default();
|
||||||
|
for server in manifest.remote_mcp_servers_config {
|
||||||
|
let Some(uuid) = server.uuid else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !attributed_server_ids.contains(&uuid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(name) = super::normalized_connector_display_name(server.name.as_deref())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
connector_names.entry(name.to_lowercase()).or_insert(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connector_names_by_session
|
||||||
|
.into_iter()
|
||||||
|
.map(|(session_id, names)| (session_id, names.into_values().collect()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_session_manifest(path: &Path) -> Option<SessionManifest> {
|
||||||
|
let contents = fs::read_to_string(path).ok()?;
|
||||||
|
serde_json::from_str(&contents).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_files_recursively(root: &Path) -> Vec<PathBuf> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
let mut pending = vec![root.to_path_buf()];
|
||||||
|
while let Some(directory) = pending.pop() {
|
||||||
|
let Ok(entries) = fs::read_dir(directory) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let Ok(file_type) = entry.file_type() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if file_type.is_dir() {
|
||||||
|
pending.push(entry.path());
|
||||||
|
} else if file_type.is_file()
|
||||||
|
&& entry.path().extension().and_then(|value| value.to_str()) == Some("json")
|
||||||
|
{
|
||||||
|
files.push(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ use codex_protocol::protocol::TurnCompleteEvent;
|
|||||||
use codex_protocol::protocol::TurnStartedEvent;
|
use codex_protocol::protocol::TurnStartedEvent;
|
||||||
use codex_protocol::protocol::UserMessageEvent;
|
use codex_protocol::protocol::UserMessageEvent;
|
||||||
use codex_utils_output_truncation::approx_tokens_from_byte_count_i64;
|
use codex_utils_output_truncation::approx_tokens_from_byte_count_i64;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -25,19 +26,18 @@ const EXTERNAL_SESSION_IMPORTED_MARKER: &str = "<EXTERNAL SESSION IMPORTED>";
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn load_session_for_import(path: &Path) -> io::Result<Option<ImportedExternalAgentSession>> {
|
fn load_session_for_import(path: &Path) -> io::Result<Option<ImportedExternalAgentSession>> {
|
||||||
Ok(
|
Ok(load_session_for_import_with_content_sha256(path)?
|
||||||
load_session_for_import_with_content_sha256(path)?
|
.map(|(session, _content_sha256, _attributed_mcp_server_ids)| session))
|
||||||
.map(|(session, _content_sha256)| session),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load_session_for_import_with_content_sha256(
|
pub(crate) fn load_session_for_import_with_content_sha256(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> io::Result<Option<(ImportedExternalAgentSession, String)>> {
|
) -> io::Result<Option<(ImportedExternalAgentSession, String, BTreeSet<String>)>> {
|
||||||
let parsed = read_session_import(path)?;
|
let parsed = read_session_import(path)?;
|
||||||
let Some(cwd) = parsed.cwd else {
|
let Some(cwd) = parsed.cwd else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
let attributed_mcp_server_ids = parsed.attributed_mcp_server_ids;
|
||||||
let messages = parsed.messages;
|
let messages = parsed.messages;
|
||||||
let first_user_message_text = messages
|
let first_user_message_text = messages
|
||||||
.iter()
|
.iter()
|
||||||
@@ -67,6 +67,7 @@ pub(crate) fn load_session_for_import_with_content_sha256(
|
|||||||
rollout_items,
|
rollout_items,
|
||||||
},
|
},
|
||||||
parsed.content_sha256,
|
parsed.content_sha256,
|
||||||
|
attributed_mcp_server_ids,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use serde::Deserialize;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@@ -28,6 +29,8 @@ struct ImportedExternalAgentSessionRecord {
|
|||||||
imported_at: i64,
|
imported_at: i64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
source_modified_at: Option<i64>,
|
source_modified_at: Option<i64>,
|
||||||
|
#[serde(default)]
|
||||||
|
connector_names: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
@@ -35,6 +38,13 @@ pub struct CompletedExternalAgentSessionImport {
|
|||||||
pub source_path: PathBuf,
|
pub source_path: PathBuf,
|
||||||
pub source_content_sha256: String,
|
pub source_content_sha256: String,
|
||||||
pub imported_thread_id: ThreadId,
|
pub imported_thread_id: ThreadId,
|
||||||
|
pub connector_names: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ImportedConnectorCandidate {
|
||||||
|
pub name: String,
|
||||||
|
pub session_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -63,6 +73,7 @@ pub(crate) fn record_imported_session(
|
|||||||
source_content_sha256: session_content_sha256(&source_path)?,
|
source_content_sha256: session_content_sha256(&source_path)?,
|
||||||
source_path,
|
source_path,
|
||||||
imported_thread_id,
|
imported_thread_id,
|
||||||
|
connector_names: Vec::new(),
|
||||||
}],
|
}],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -86,6 +97,7 @@ pub fn record_completed_session_imports(
|
|||||||
record.imported_thread_id = import.imported_thread_id;
|
record.imported_thread_id = import.imported_thread_id;
|
||||||
record.imported_at = imported_at;
|
record.imported_at = imported_at;
|
||||||
record.source_modified_at = source_modified_at.or(record.source_modified_at);
|
record.source_modified_at = source_modified_at.or(record.source_modified_at);
|
||||||
|
record.connector_names = import.connector_names;
|
||||||
ledger.records.push(record);
|
ledger.records.push(record);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -95,11 +107,42 @@ pub fn record_completed_session_imports(
|
|||||||
imported_thread_id: import.imported_thread_id,
|
imported_thread_id: import.imported_thread_id,
|
||||||
imported_at,
|
imported_at,
|
||||||
source_modified_at,
|
source_modified_at,
|
||||||
|
connector_names: import.connector_names,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
save_import_ledger(codex_home, &ledger)
|
save_import_ledger(codex_home, &ledger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_imported_connector_candidates(
|
||||||
|
codex_home: &Path,
|
||||||
|
) -> io::Result<Vec<ImportedConnectorCandidate>> {
|
||||||
|
let ledger = load_import_ledger(codex_home)?;
|
||||||
|
let mut connector_names_by_source = BTreeMap::new();
|
||||||
|
for record in ledger.records {
|
||||||
|
connector_names_by_source.insert(record.source_path, record.connector_names);
|
||||||
|
}
|
||||||
|
let mut candidates_by_name = BTreeMap::<String, ImportedConnectorCandidate>::new();
|
||||||
|
for connector_names in connector_names_by_source.into_values() {
|
||||||
|
let connector_names = connector_names
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|name| super::normalized_connector_display_name(Some(&name)))
|
||||||
|
.map(|name| (name.to_lowercase(), name))
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
for (key, name) in connector_names {
|
||||||
|
let candidate = candidates_by_name
|
||||||
|
.entry(key)
|
||||||
|
.or_insert(ImportedConnectorCandidate {
|
||||||
|
name,
|
||||||
|
session_count: 0,
|
||||||
|
});
|
||||||
|
candidate.session_count = candidate.session_count.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut candidates = candidates_by_name.into_values().collect::<Vec<_>>();
|
||||||
|
candidates.sort_by(|left, right| left.name.cmp(&right.name));
|
||||||
|
Ok(candidates)
|
||||||
|
}
|
||||||
|
|
||||||
impl ImportedExternalAgentSessionLedger {
|
impl ImportedExternalAgentSessionLedger {
|
||||||
pub(super) fn source_states(&self) -> HashMap<&Path, ImportedSourceState> {
|
pub(super) fn source_states(&self) -> HashMap<&Path, ImportedSourceState> {
|
||||||
let mut states = HashMap::new();
|
let mut states = HashMap::new();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use super::CompletedExternalAgentSessionImport;
|
use super::CompletedExternalAgentSessionImport;
|
||||||
|
use super::ImportedConnectorCandidate;
|
||||||
use super::ImportedExternalAgentSessionLedger;
|
use super::ImportedExternalAgentSessionLedger;
|
||||||
|
use super::read_imported_connector_candidates;
|
||||||
use super::record_completed_session_imports;
|
use super::record_completed_session_imports;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
@@ -35,6 +37,7 @@ fn completed_imports_do_not_read_source_files() {
|
|||||||
source_path: source_path.clone(),
|
source_path: source_path.clone(),
|
||||||
source_content_sha256: format!("{:x}", Sha256::digest(contents)),
|
source_content_sha256: format!("{:x}", Sha256::digest(contents)),
|
||||||
imported_thread_id,
|
imported_thread_id,
|
||||||
|
connector_names: Vec::new(),
|
||||||
}],
|
}],
|
||||||
)
|
)
|
||||||
.expect("record completed imports");
|
.expect("record completed imports");
|
||||||
@@ -64,6 +67,7 @@ fn completed_import_refreshes_existing_record_metadata() {
|
|||||||
source_path: source_path.clone(),
|
source_path: source_path.clone(),
|
||||||
source_content_sha256: content_sha256.clone(),
|
source_content_sha256: content_sha256.clone(),
|
||||||
imported_thread_id: first_thread_id,
|
imported_thread_id: first_thread_id,
|
||||||
|
connector_names: vec!["Gmail".to_string()],
|
||||||
}],
|
}],
|
||||||
)
|
)
|
||||||
.expect("record first import");
|
.expect("record first import");
|
||||||
@@ -73,6 +77,7 @@ fn completed_import_refreshes_existing_record_metadata() {
|
|||||||
source_path: source_path.clone(),
|
source_path: source_path.clone(),
|
||||||
source_content_sha256: content_sha256,
|
source_content_sha256: content_sha256,
|
||||||
imported_thread_id: second_thread_id,
|
imported_thread_id: second_thread_id,
|
||||||
|
connector_names: vec!["Slack".to_string()],
|
||||||
}],
|
}],
|
||||||
)
|
)
|
||||||
.expect("record replacement import");
|
.expect("record replacement import");
|
||||||
@@ -82,4 +87,52 @@ fn completed_import_refreshes_existing_record_metadata() {
|
|||||||
assert_eq!(ledger.records[0].source_path, source_path);
|
assert_eq!(ledger.records[0].source_path, source_path);
|
||||||
assert_eq!(ledger.records[0].imported_thread_id, second_thread_id);
|
assert_eq!(ledger.records[0].imported_thread_id, second_thread_id);
|
||||||
assert!(ledger.records[0].source_modified_at.is_some());
|
assert!(ledger.records[0].source_modified_at.is_some());
|
||||||
|
assert_eq!(ledger.records[0].connector_names, vec!["Slack"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connector_candidates_use_latest_import_for_each_source() {
|
||||||
|
let root = TempDir::new().expect("tempdir");
|
||||||
|
let codex_home = root.path().join("codex-home");
|
||||||
|
let first_source = root.path().join("first.jsonl");
|
||||||
|
let second_source = root.path().join("second.jsonl");
|
||||||
|
|
||||||
|
record_completed_session_imports(
|
||||||
|
&codex_home,
|
||||||
|
vec![
|
||||||
|
CompletedExternalAgentSessionImport {
|
||||||
|
source_path: first_source.clone(),
|
||||||
|
source_content_sha256: "first-version".to_string(),
|
||||||
|
imported_thread_id: ThreadId::new(),
|
||||||
|
connector_names: vec!["Gmail".to_string()],
|
||||||
|
},
|
||||||
|
CompletedExternalAgentSessionImport {
|
||||||
|
source_path: first_source,
|
||||||
|
source_content_sha256: "second-version".to_string(),
|
||||||
|
imported_thread_id: ThreadId::new(),
|
||||||
|
connector_names: vec!["Slack".to_string()],
|
||||||
|
},
|
||||||
|
CompletedExternalAgentSessionImport {
|
||||||
|
source_path: second_source,
|
||||||
|
source_content_sha256: "only-version".to_string(),
|
||||||
|
imported_thread_id: ThreadId::new(),
|
||||||
|
connector_names: vec!["Gmail".to_string(), "Slack".to_string()],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.expect("record imports");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
read_imported_connector_candidates(&codex_home).expect("read connector candidates"),
|
||||||
|
vec![
|
||||||
|
ImportedConnectorCandidate {
|
||||||
|
name: "Gmail".to_string(),
|
||||||
|
session_count: 1,
|
||||||
|
},
|
||||||
|
ImportedConnectorCandidate {
|
||||||
|
name: "Slack".to_string(),
|
||||||
|
session_count: 2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Parsing and export helpers for external-agent session histories.
|
//! Parsing and export helpers for external-agent session histories.
|
||||||
|
|
||||||
|
mod connectors_cla;
|
||||||
mod detect_cla;
|
mod detect_cla;
|
||||||
mod export;
|
mod export;
|
||||||
mod ledger;
|
mod ledger;
|
||||||
@@ -7,20 +8,31 @@ mod records;
|
|||||||
mod title;
|
mod title;
|
||||||
|
|
||||||
use codex_protocol::protocol::RolloutItem;
|
use codex_protocol::protocol::RolloutItem;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub use connectors_cla::ImportedSessionConnectorAttribution;
|
||||||
|
pub use connectors_cla::detect_imported_cla_session_connectors;
|
||||||
pub use detect_cla::detect_recent_cla_sessions;
|
pub use detect_cla::detect_recent_cla_sessions;
|
||||||
use export::load_session_for_import_with_content_sha256;
|
use export::load_session_for_import_with_content_sha256;
|
||||||
pub use ledger::CompletedExternalAgentSessionImport;
|
pub use ledger::CompletedExternalAgentSessionImport;
|
||||||
|
pub use ledger::ImportedConnectorCandidate;
|
||||||
pub use ledger::has_current_session_been_imported;
|
pub use ledger::has_current_session_been_imported;
|
||||||
|
pub use ledger::read_imported_connector_candidates;
|
||||||
pub use ledger::record_completed_session_imports;
|
pub use ledger::record_completed_session_imports;
|
||||||
pub use records::SessionSummary;
|
pub use records::SessionSummary;
|
||||||
pub use records::summarize_session;
|
pub use records::summarize_session;
|
||||||
|
|
||||||
const SESSION_TITLE_MAX_LEN: usize = 120;
|
const SESSION_TITLE_MAX_LEN: usize = 120;
|
||||||
|
|
||||||
|
fn normalized_connector_display_name(name: Option<&str>) -> Option<String> {
|
||||||
|
name.map(str::trim)
|
||||||
|
.filter(|name| !name.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ExternalAgentSessionMigration {
|
pub struct ExternalAgentSessionMigration {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
@@ -40,6 +52,7 @@ pub struct ImportedExternalAgentSession {
|
|||||||
pub struct PendingSessionImport {
|
pub struct PendingSessionImport {
|
||||||
pub source_path: PathBuf,
|
pub source_path: PathBuf,
|
||||||
pub source_content_sha256: String,
|
pub source_content_sha256: String,
|
||||||
|
pub attributed_mcp_server_ids: BTreeSet<String>,
|
||||||
pub session: ImportedExternalAgentSession,
|
pub session: ImportedExternalAgentSession,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,32 +64,25 @@ pub fn prepare_validated_session_import(
|
|||||||
if has_been_imported {
|
if has_been_imported {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let Some((source_path, imported_session, source_content_sha256)) =
|
load_importable_session(&session.path)
|
||||||
load_importable_session(&session.path)?
|
|
||||||
else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
Ok(Some(PendingSessionImport {
|
|
||||||
source_path,
|
|
||||||
source_content_sha256,
|
|
||||||
session: imported_session,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_importable_session(
|
fn load_importable_session(path: &Path) -> io::Result<Option<PendingSessionImport>> {
|
||||||
path: &Path,
|
|
||||||
) -> io::Result<Option<(PathBuf, ImportedExternalAgentSession, String)>> {
|
|
||||||
let source_path = std::fs::canonicalize(path)?;
|
let source_path = std::fs::canonicalize(path)?;
|
||||||
let Some((imported_session, source_content_sha256)) =
|
let Some((imported_session, source_content_sha256, attributed_mcp_server_ids)) =
|
||||||
load_session_for_import_with_content_sha256(&source_path)?
|
load_session_for_import_with_content_sha256(&source_path)?
|
||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
Ok(imported_session.cwd.is_dir().then_some((
|
Ok(imported_session
|
||||||
source_path,
|
.cwd
|
||||||
imported_session,
|
.is_dir()
|
||||||
source_content_sha256,
|
.then_some(PendingSessionImport {
|
||||||
)))
|
source_path,
|
||||||
|
source_content_sha256,
|
||||||
|
attributed_mcp_server_ids,
|
||||||
|
session: imported_session,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use super::truncate;
|
|||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
@@ -31,6 +32,7 @@ pub(super) struct ParsedSessionImport {
|
|||||||
pub ai_title: Option<String>,
|
pub ai_title: Option<String>,
|
||||||
pub messages: Vec<ConversationMessage>,
|
pub messages: Vec<ConversationMessage>,
|
||||||
pub content_sha256: String,
|
pub content_sha256: String,
|
||||||
|
pub attributed_mcp_server_ids: BTreeSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn summarize_session(path: &Path) -> io::Result<Option<SessionSummary>> {
|
pub fn summarize_session(path: &Path) -> io::Result<Option<SessionSummary>> {
|
||||||
@@ -114,6 +116,7 @@ pub(super) fn read_session_import(path: &Path) -> io::Result<ParsedSessionImport
|
|||||||
let mut custom_title = None;
|
let mut custom_title = None;
|
||||||
let mut ai_title = None;
|
let mut ai_title = None;
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
|
let mut attributed_mcp_server_ids = BTreeSet::new();
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
loop {
|
loop {
|
||||||
@@ -129,6 +132,14 @@ pub(super) fn read_session_import(path: &Path) -> io::Result<ParsedSessionImport
|
|||||||
let Ok(mut record) = serde_json::from_str::<JsonValue>(trimmed) else {
|
let Ok(mut record) = serde_json::from_str::<JsonValue>(trimmed) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
if let Some(server_id) = record
|
||||||
|
.get("attributionMcpServer")
|
||||||
|
.and_then(JsonValue::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|server_id| !server_id.is_empty())
|
||||||
|
{
|
||||||
|
attributed_mcp_server_ids.insert(server_id.to_string());
|
||||||
|
}
|
||||||
if cwd.is_none() {
|
if cwd.is_none() {
|
||||||
cwd = record
|
cwd = record
|
||||||
.get("cwd")
|
.get("cwd")
|
||||||
@@ -151,6 +162,7 @@ pub(super) fn read_session_import(path: &Path) -> io::Result<ParsedSessionImport
|
|||||||
ai_title,
|
ai_title,
|
||||||
messages,
|
messages,
|
||||||
content_sha256: format!("{:x}", hasher.finalize()),
|
content_sha256: format!("{:x}", hasher.finalize()),
|
||||||
|
attributed_mcp_server_ids,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user