@geminixiang/mikan 1.0.0-beta.34 → 1.0.0-beta.35

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.
@@ -1 +1 @@
1
- {"version":3,"file":"office-migration.js","sourceRoot":"","sources":["../src/office-migration.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, lstatSync, readdirSync, renameSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport * as log from \"./log.js\";\nimport { isOfficeKey, officeStateDir } from \"./office-address.js\";\nimport { OfficeRegistry } from \"./office-registry.js\";\nimport { migrateConversationVaultKeys } from \"./vault/index.js\";\nimport type { OfficeMigrationRecord, PlatformName } from \"./types.js\";\n\n/**\n * Workspace-root entries that are shared infrastructure, never office dirs.\n * Mirrors the boot-time directory setup in main.ts.\n */\nconst RESERVED_WORKSPACE_ENTRIES = new Set([\"skills\", \"events\", \"agents\"]);\n\nexport interface OfficeMigrationRunSummary {\n /** Raw ids whose directories moved to the office-key layout this run. */\n migrated: string[];\n /** Raw ids whose interrupted moves were completed from the journal. */\n recovered: string[];\n /** Raw ids that still need an explicit owner (`mikan office claim`). */\n unowned: string[];\n /** Raw ids whose records are failed and need operator repair. */\n failed: string[];\n /** Raw ids whose conversation vault directories moved to office keys. */\n vaultKeysMigrated: string[];\n /** Raw ids whose legacy and office-key vault dirs both exist (manual merge). */\n vaultConflicts: string[];\n /** Raw ids whose host state dirs (settings, extensions, data) moved. */\n stateDirsMigrated: string[];\n /** Raw ids whose legacy and office-key state dirs both exist (manual merge). */\n stateDirConflicts: string[];\n}\n\n/**\n * Move every legacy raw-id office directory under the workspace root to the\n * canonical office-key layout, journaling each move through the office\n * registry so an interrupted run resumes instead of losing offices.\n *\n * The engine never guesses: with several enabled platforms an unowned raw\n * directory stays in place and is reported for `mikan office claim`. Callers\n * must treat a non-empty `unowned`/`failed` as fatal for daemon boot — after\n * the layout flip, an unmigrated legacy directory is invisible to the\n * runtime, which would silently present the conversation as empty.\n */\nexport function migrateLegacyOffices(options: {\n workspaceRoot: string;\n stateDir: string;\n enabledPlatforms: readonly PlatformName[];\n}): OfficeMigrationRunSummary {\n const registry = new OfficeRegistry(options.stateDir);\n for (const platform of options.enabledPlatforms) registry.enablePlatform(platform);\n\n const summary: OfficeMigrationRunSummary = {\n migrated: [],\n recovered: [],\n unowned: [],\n failed: [],\n vaultKeysMigrated: [],\n vaultConflicts: [],\n stateDirsMigrated: [],\n stateDirConflicts: [],\n };\n\n recoverInterruptedMoves(registry, summary);\n claimAndMoveLegacyDirs(registry, options.workspaceRoot, summary);\n\n // Credential vaults are keyed by office too; the registry inventory (which\n // the directory moves above just extended) drives the same rename.\n const vaults = migrateConversationVaultKeys({\n stateDir: options.stateDir,\n offices: registry.getOffices(),\n });\n summary.vaultKeysMigrated = vaults.migrated;\n summary.vaultConflicts = vaults.conflicts;\n for (const rawConversationId of vaults.migrated) {\n log.logInfo(`[office] Migrated vault key to office key: ${rawConversationId}`);\n }\n\n migrateConversationStateDirs(registry, options.stateDir, summary);\n\n for (const record of registry.getState().migrations) {\n if (record.status === \"needs-owner\") summary.unowned.push(record.rawConversationId);\n else if (record.status === \"failed\") summary.failed.push(record.rawConversationId);\n }\n return summary;\n}\n\n/** Complete or fail every move the journal says was interrupted mid-rename. */\nfunction recoverInterruptedMoves(\n registry: OfficeRegistry,\n summary: OfficeMigrationRunSummary,\n): void {\n for (const record of registry.getState().migrations) {\n if (record.status !== \"moving\") continue;\n const targetDir = record.targetDir;\n if (!targetDir) {\n registry.markFailed(record.rawConversationId, \"Moving record has no target directory\");\n continue;\n }\n const sourceExists = pathExists(record.sourceDir);\n const targetExists = pathExists(targetDir);\n if (sourceExists && targetExists) {\n registry.markFailed(\n record.rawConversationId,\n \"Both legacy source and office target exist; merge them manually\",\n );\n continue;\n }\n if (!sourceExists && !targetExists) {\n registry.markFailed(\n record.rawConversationId,\n \"Neither legacy source nor office target exists\",\n );\n continue;\n }\n if (sourceExists) renameSync(record.sourceDir, targetDir);\n registry.markCommitted(record.rawConversationId);\n recordMigratedOffice(registry, record);\n summary.recovered.push(record.rawConversationId);\n log.logInfo(`[office] Recovered interrupted migration: ${record.rawConversationId}`);\n }\n}\n\n/** Scan the workspace root and move every claimable legacy office directory. */\nfunction claimAndMoveLegacyDirs(\n registry: OfficeRegistry,\n workspaceRoot: string,\n summary: OfficeMigrationRunSummary,\n): void {\n for (const rawConversationId of listLegacyOfficeDirs(workspaceRoot)) {\n const existing = registry.getMigration(rawConversationId);\n if (existing?.status === \"committed\") {\n // The move already happened; a raw-id dir with this name reappearing\n // afterwards is a different (or resurrected) office the runtime can no\n // longer see. Refusing to boot beats silently ignoring its data.\n throw new Error(\n `Legacy office directory reappeared after migration: ${join(workspaceRoot, rawConversationId)}`,\n );\n }\n if (existing?.status === \"failed\") continue; // reported via summary.failed\n\n const record = registry.prepareLegacyMigration({\n rawConversationId,\n sourceDir: join(workspaceRoot, rawConversationId),\n workspaceRoot,\n });\n if (record.status === \"needs-owner\") continue; // reported via summary.unowned\n\n const moving = registry.markMoving(rawConversationId);\n const targetDir = moving.targetDir;\n if (!targetDir) throw new Error(\"Moving office migration has no target directory\");\n renameSync(moving.sourceDir, targetDir);\n registry.markCommitted(rawConversationId);\n recordMigratedOffice(registry, moving);\n summary.migrated.push(rawConversationId);\n log.logInfo(`[office] Migrated office directory: ${rawConversationId} -> ${targetDir}`);\n }\n}\n\n/**\n * Move each registered office's host state tree — settings.json, extensions,\n * extension-data, git checkouts — from `conversations/<rawId>` to\n * `conversations/<officeKey>` in one rename. Conflicts are reported for\n * manual merge, never clobbered.\n */\nfunction migrateConversationStateDirs(\n registry: OfficeRegistry,\n stateDir: string,\n summary: OfficeMigrationRunSummary,\n): void {\n for (const office of registry.getOffices()) {\n const legacyDir = join(stateDir, \"conversations\", office.conversationId);\n if (!existsSync(legacyDir)) continue;\n const targetDir = officeStateDir(stateDir, office);\n if (existsSync(targetDir)) {\n summary.stateDirConflicts.push(office.conversationId);\n continue;\n }\n renameSync(legacyDir, targetDir);\n summary.stateDirsMigrated.push(office.conversationId);\n log.logInfo(`[office] Migrated host state dir to office key: ${office.conversationId}`);\n }\n}\n\n/** A migrated office is an existing office; keep the directory enumerable. */\nfunction recordMigratedOffice(registry: OfficeRegistry, record: OfficeMigrationRecord): void {\n if (!record.ownerPlatform) return;\n registry.recordOffice({\n platform: record.ownerPlatform,\n conversationId: record.rawConversationId,\n });\n}\n\n/**\n * Legacy office candidates: regular directories that are not reserved\n * infrastructure, not hidden, and not already office-key named. A symlink in\n * office position is refused outright — following it could move data from\n * outside the workspace root.\n *\n * A candidate must also look like a conversation office: every office that\n * ever saw a message has a `log.jsonl`, and every office that ran the agent\n * has `sessions/`. Trusted workspace roots legitimately accumulate other\n * directories — repos the agent cloned, build output — which are not offices\n * and must be neither renamed nor reported as unowned; they stay where they\n * are (an operator can still claim one explicitly via `mikan office claim`).\n */\nfunction listLegacyOfficeDirs(workspaceRoot: string): string[] {\n const candidates: string[] = [];\n for (const entry of readdirSync(workspaceRoot, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n if (RESERVED_WORKSPACE_ENTRIES.has(entry.name)) continue;\n if (isOfficeKey(entry.name)) continue;\n if (entry.isSymbolicLink()) {\n throw new Error(`Workspace entry must not be a symlink: ${join(workspaceRoot, entry.name)}`);\n }\n if (!entry.isDirectory()) continue;\n if (!looksLikeConversationOffice(join(workspaceRoot, entry.name))) {\n log.logInfo(\n `[office] Skipping non-office workspace directory (no log.jsonl or sessions/): ${entry.name}`,\n );\n continue;\n }\n candidates.push(entry.name);\n }\n return candidates.toSorted();\n}\n\nfunction looksLikeConversationOffice(dir: string): boolean {\n return existsSync(join(dir, \"log.jsonl\")) || existsSync(join(dir, \"sessions\"));\n}\n\nfunction pathExists(path: string): boolean {\n try {\n lstatSync(path);\n return true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw err;\n }\n}\n\n/** Operator-facing boot failure for offices the engine may not move itself. */\nexport function formatUnmigratedOfficesError(summary: OfficeMigrationRunSummary): string {\n const lines = [\"Conversation office migration cannot complete:\"];\n if (summary.unowned.length > 0) {\n lines.push(\n \"\",\n \"These legacy conversation directories have no owning platform (several\",\n \"platforms are enabled, so ownership cannot be inferred):\",\n ...summary.unowned.map((id) => ` - ${id}`),\n \"\",\n \"Assign each one with:\",\n \" mikan office claim <conversationId> <platform>\",\n );\n }\n if (summary.failed.length > 0) {\n lines.push(\n \"\",\n \"These migrations previously failed and need manual repair (see\",\n \"office-registry.json in the state dir for each error):\",\n ...summary.failed.map((id) => ` - ${id}`),\n );\n }\n if (summary.vaultConflicts.length > 0) {\n lines.push(\n \"\",\n \"These conversations have credentials under both the legacy and the\",\n \"office-key vault directory; merge them manually under <state-dir>/vaults:\",\n ...summary.vaultConflicts.map((id) => ` - ${id}`),\n );\n }\n if (summary.stateDirConflicts.length > 0) {\n lines.push(\n \"\",\n \"These conversations have host state under both the legacy and the\",\n \"office-key directory; merge them manually under <state-dir>/conversations:\",\n ...summary.stateDirConflicts.map((id) => ` - ${id}`),\n );\n }\n return lines.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"office-migration.js","sourceRoot":"","sources":["../src/office-migration.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, lstatSync, readdirSync, renameSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport * as log from \"./log.js\";\nimport { isOfficeKey, officeKey, officeStateDir } from \"./office-address.js\";\nimport { legacyConversationCredentialKey } from \"./sandbox/identity.js\";\nimport type { OfficeRecord } from \"./types.js\";\nimport { OfficeRegistry } from \"./office-registry.js\";\nimport { migrateConversationVaultKeys } from \"./vault/index.js\";\nimport type { OfficeMigrationRecord, PlatformName } from \"./types.js\";\n\n/**\n * Workspace-root entries that are shared infrastructure, never office dirs.\n * Mirrors the boot-time directory setup in main.ts.\n */\nconst RESERVED_WORKSPACE_ENTRIES = new Set([\"skills\", \"events\", \"agents\"]);\n\nexport interface OfficeMigrationRunSummary {\n /** Raw ids whose directories moved to the office-key layout this run. */\n migrated: string[];\n /** Raw ids whose interrupted moves were completed from the journal. */\n recovered: string[];\n /** Raw ids that still need an explicit owner (`mikan office claim`). */\n unowned: string[];\n /** Raw ids whose records are failed and need operator repair. */\n failed: string[];\n /** Raw ids whose conversation vault directories moved to office keys. */\n vaultKeysMigrated: string[];\n /** Raw ids whose legacy and office-key vault dirs both exist (manual merge). */\n vaultConflicts: string[];\n /** Raw ids whose host state dirs (settings, extensions, data) moved. */\n stateDirsMigrated: string[];\n /** Raw ids whose legacy and office-key state dirs both exist (manual merge). */\n stateDirConflicts: string[];\n}\n\n/**\n * Move every legacy raw-id office directory under the workspace root to the\n * canonical office-key layout, journaling each move through the office\n * registry so an interrupted run resumes instead of losing offices.\n *\n * The engine never guesses: with several enabled platforms an unowned raw\n * directory stays in place and is reported for `mikan office claim`. Callers\n * must treat a non-empty `unowned`/`failed` as fatal for daemon boot — after\n * the layout flip, an unmigrated legacy directory is invisible to the\n * runtime, which would silently present the conversation as empty.\n */\nexport function migrateLegacyOffices(options: {\n workspaceRoot: string;\n stateDir: string;\n enabledPlatforms: readonly PlatformName[];\n}): OfficeMigrationRunSummary {\n const registry = new OfficeRegistry(options.stateDir);\n for (const platform of options.enabledPlatforms) registry.enablePlatform(platform);\n\n const summary: OfficeMigrationRunSummary = {\n migrated: [],\n recovered: [],\n unowned: [],\n failed: [],\n vaultKeysMigrated: [],\n vaultConflicts: [],\n stateDirsMigrated: [],\n stateDirConflicts: [],\n };\n\n recoverInterruptedMoves(registry, summary);\n claimAndMoveLegacyDirs(registry, options.workspaceRoot, summary);\n\n // Credential vaults are keyed by office too; the registry inventory (which\n // the directory moves above just extended) drives the same rename.\n const vaults = migrateConversationVaultKeys({\n stateDir: options.stateDir,\n offices: registry.getOffices(),\n });\n summary.vaultKeysMigrated = vaults.migrated;\n summary.vaultConflicts = vaults.conflicts;\n for (const rawConversationId of vaults.migrated) {\n log.logInfo(`[office] Migrated vault key to office key: ${rawConversationId}`);\n }\n\n migrateConversationStateDirs(registry, options.stateDir, summary);\n\n for (const record of registry.getState().migrations) {\n if (record.status === \"needs-owner\") summary.unowned.push(record.rawConversationId);\n else if (record.status === \"failed\") summary.failed.push(record.rawConversationId);\n }\n return summary;\n}\n\n/** Complete or fail every move the journal says was interrupted mid-rename. */\nfunction recoverInterruptedMoves(\n registry: OfficeRegistry,\n summary: OfficeMigrationRunSummary,\n): void {\n for (const record of registry.getState().migrations) {\n if (record.status !== \"moving\") continue;\n const targetDir = record.targetDir;\n if (!targetDir) {\n registry.markFailed(record.rawConversationId, \"Moving record has no target directory\");\n continue;\n }\n const sourceExists = pathExists(record.sourceDir);\n const targetExists = pathExists(targetDir);\n if (sourceExists && targetExists) {\n registry.markFailed(\n record.rawConversationId,\n \"Both legacy source and office target exist; merge them manually\",\n );\n continue;\n }\n if (!sourceExists && !targetExists) {\n registry.markFailed(\n record.rawConversationId,\n \"Neither legacy source nor office target exists\",\n );\n continue;\n }\n if (sourceExists) renameSync(record.sourceDir, targetDir);\n registry.markCommitted(record.rawConversationId);\n recordMigratedOffice(registry, record);\n summary.recovered.push(record.rawConversationId);\n log.logInfo(`[office] Recovered interrupted migration: ${record.rawConversationId}`);\n }\n}\n\n/** Scan the workspace root and move every claimable legacy office directory. */\nfunction claimAndMoveLegacyDirs(\n registry: OfficeRegistry,\n workspaceRoot: string,\n summary: OfficeMigrationRunSummary,\n): void {\n for (const rawConversationId of listLegacyOfficeDirs(workspaceRoot)) {\n const existing = registry.getMigration(rawConversationId);\n if (existing?.status === \"committed\") {\n // The move already happened; a raw-id dir with this name reappearing\n // afterwards is a different (or resurrected) office the runtime can no\n // longer see. Refusing to boot beats silently ignoring its data.\n throw new Error(\n `Legacy office directory reappeared after migration: ${join(workspaceRoot, rawConversationId)}`,\n );\n }\n if (existing?.status === \"failed\") continue; // reported via summary.failed\n\n const record = registry.prepareLegacyMigration({\n rawConversationId,\n sourceDir: join(workspaceRoot, rawConversationId),\n workspaceRoot,\n });\n if (record.status === \"needs-owner\") continue; // reported via summary.unowned\n\n const moving = registry.markMoving(rawConversationId);\n const targetDir = moving.targetDir;\n if (!targetDir) throw new Error(\"Moving office migration has no target directory\");\n renameSync(moving.sourceDir, targetDir);\n registry.markCommitted(rawConversationId);\n recordMigratedOffice(registry, moving);\n summary.migrated.push(rawConversationId);\n log.logInfo(`[office] Migrated office directory: ${rawConversationId} -> ${targetDir}`);\n }\n}\n\n/**\n * Move each registered office's host state tree — settings.json, extensions,\n * extension-data, git checkouts — from `conversations/<rawId>` to\n * `conversations/<officeKey>` in one rename. Conflicts are reported for\n * manual merge, never clobbered.\n */\nfunction migrateConversationStateDirs(\n registry: OfficeRegistry,\n stateDir: string,\n summary: OfficeMigrationRunSummary,\n): void {\n for (const office of registry.getOffices()) {\n const legacyDir = join(stateDir, \"conversations\", office.conversationId);\n if (!existsSync(legacyDir)) continue;\n const targetDir = officeStateDir(stateDir, office);\n if (existsSync(targetDir)) {\n summary.stateDirConflicts.push(office.conversationId);\n continue;\n }\n renameSync(legacyDir, targetDir);\n summary.stateDirsMigrated.push(office.conversationId);\n log.logInfo(`[office] Migrated host state dir to office key: ${office.conversationId}`);\n }\n}\n\n/** A migrated office is an existing office; keep the directory enumerable. */\nfunction recordMigratedOffice(registry: OfficeRegistry, record: OfficeMigrationRecord): void {\n if (!record.ownerPlatform) return;\n registry.recordOffice({\n platform: record.ownerPlatform,\n conversationId: record.rawConversationId,\n });\n}\n\n/**\n * Legacy office candidates: regular directories that are not reserved\n * infrastructure, not hidden, and not already office-key named. A symlink in\n * office position is refused outright — following it could move data from\n * outside the workspace root.\n *\n * A candidate must also look like a conversation office: every office that\n * ever saw a message has a `log.jsonl`, and every office that ran the agent\n * has `sessions/`. Trusted workspace roots legitimately accumulate other\n * directories — repos the agent cloned, build output — which are not offices\n * and must be neither renamed nor reported as unowned; they stay where they\n * are (an operator can still claim one explicitly via `mikan office claim`).\n */\nfunction listLegacyOfficeDirs(workspaceRoot: string): string[] {\n const candidates: string[] = [];\n for (const entry of readdirSync(workspaceRoot, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n if (RESERVED_WORKSPACE_ENTRIES.has(entry.name)) continue;\n if (isOfficeKey(entry.name)) continue;\n if (entry.isSymbolicLink()) {\n throw new Error(`Workspace entry must not be a symlink: ${join(workspaceRoot, entry.name)}`);\n }\n if (!entry.isDirectory()) continue;\n if (!looksLikeConversationOffice(join(workspaceRoot, entry.name))) {\n log.logInfo(\n `[office] Skipping non-office workspace directory (no log.jsonl or sessions/): ${entry.name}`,\n );\n continue;\n }\n candidates.push(entry.name);\n }\n return candidates.toSorted();\n}\n\nfunction looksLikeConversationOffice(dir: string): boolean {\n return existsSync(join(dir, \"log.jsonl\")) || existsSync(join(dir, \"sessions\"));\n}\n\nfunction pathExists(path: string): boolean {\n try {\n lstatSync(path);\n return true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw err;\n }\n}\n\n/** Operator-facing boot failure for offices the engine may not move itself. */\nexport function formatUnmigratedOfficesError(summary: OfficeMigrationRunSummary): string {\n const lines = [\"Conversation office migration cannot complete:\"];\n if (summary.unowned.length > 0) {\n lines.push(\n \"\",\n \"These legacy conversation directories have no owning platform (several\",\n \"platforms are enabled, so ownership cannot be inferred):\",\n ...summary.unowned.map((id) => ` - ${id}`),\n \"\",\n \"Assign each one with:\",\n \" mikan office claim <conversationId> <platform>\",\n );\n }\n if (summary.failed.length > 0) {\n lines.push(\n \"\",\n \"These migrations previously failed and need manual repair (see\",\n \"office-registry.json in the state dir for each error):\",\n ...summary.failed.map((id) => ` - ${id}`),\n );\n }\n if (summary.vaultConflicts.length > 0) {\n lines.push(\n \"\",\n \"These conversations have credentials under both the legacy and the\",\n \"office-key vault directory; merge them manually under <state-dir>/vaults:\",\n ...summary.vaultConflicts.map((id) => ` - ${id}`),\n );\n }\n if (summary.stateDirConflicts.length > 0) {\n lines.push(\n \"\",\n \"These conversations have host state under both the legacy and the\",\n \"office-key directory; merge them manually under <state-dir>/conversations:\",\n ...summary.stateDirConflicts.map((id) => ` - ${id}`),\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Bind-spec translator for the container layout migration: rewrites every\n * host path the office migration renamed — workspace dirs, per-conversation\n * state trees, conversation vaults — and the guest workspace segment, using\n * the registry's office inventory as the raw-id ↔ office mapping. Specs that\n * reference none of the renamed paths come back unchanged, which is also how\n * an already-migrated container is recognized.\n */\nfunction replacePrefix(path: string, pairs: Array<[string, string]>): string {\n for (const [oldPrefix, newPrefix] of pairs) {\n if (path === oldPrefix) return newPrefix;\n if (path.startsWith(`${oldPrefix}/`)) return newPrefix + path.slice(oldPrefix.length);\n }\n return path;\n}\n\nexport function buildContainerBindTranslator(options: {\n offices: readonly OfficeRecord[];\n workspaceRoot: string;\n stateDir: string;\n}): (bindSpec: string) => string {\n const hostPairs: Array<[string, string]> = [];\n const guestPairs: Array<[string, string]> = [];\n for (const office of options.offices) {\n const key = officeKey(office);\n const rawId = office.conversationId;\n hostPairs.push([join(options.workspaceRoot, rawId), join(options.workspaceRoot, key)]);\n hostPairs.push([\n join(options.stateDir, \"conversations\", rawId),\n officeStateDir(options.stateDir, office),\n ]);\n hostPairs.push([\n join(options.stateDir, \"vaults\", legacyConversationCredentialKey(rawId)),\n join(options.stateDir, \"vaults\", key),\n ]);\n guestPairs.push([`/workspace/${rawId}`, `/workspace/${key}`]);\n }\n\n return (bindSpec: string): string => {\n const readOnly = bindSpec.endsWith(\":ro\");\n const spec = readOnly ? bindSpec.slice(0, -3) : bindSpec;\n const separator = spec.indexOf(\":\");\n if (separator === -1) return bindSpec;\n const source = replacePrefix(spec.slice(0, separator), hostPairs);\n const target = replacePrefix(spec.slice(separator + 1), guestPairs);\n return `${source}:${target}${readOnly ? \":ro\" : \"\"}`;\n };\n}\n"]}
@@ -1,14 +1,26 @@
1
1
  export type { ContainerMount, DockerContainerManagerOptions, ProvisionOptions, ResourceLimits, SandboxLimitStatus, } from "./types.js";
2
2
  import type { DockerContainerManagerOptions, ProvisionOptions, ResourceLimits, SandboxLimitStatus } from "./types.js";
3
+ /**
4
+ * Rewrites one docker bind spec (`src:dst[:ro]`) from the legacy raw-id
5
+ * layout to the office-key layout. Returns the spec unchanged when nothing in
6
+ * it needs translation; a container whose binds are all unchanged is already
7
+ * on the current layout.
8
+ */
9
+ export type ContainerBindTranslator = (bindSpec: string) => string;
3
10
  export declare class DockerContainerManager {
4
11
  private readonly image;
5
12
  private state;
6
13
  private inflight;
14
+ /** Active layout migration; unset means no translation ever applies. */
15
+ private bindTranslator?;
16
+ private layoutMigrations;
7
17
  private static readonly MANAGED_LABEL;
8
18
  private static readonly IMAGE_MODE_LABEL;
9
19
  private static readonly VAULT_ID_LABEL_KEY;
10
20
  private static readonly CONVERSATION_ID_LABEL_KEY;
11
21
  private static readonly MOUNT_SIGNATURE_LABEL_KEY;
22
+ private static readonly MIGRATE_IMAGE_PREFIX;
23
+ private static readonly MIGRATE_BINDS_LABEL_KEY;
12
24
  private readonly limits?;
13
25
  private readonly boostLimits?;
14
26
  private readonly boostedKeys;
@@ -35,6 +47,35 @@ export declare class DockerContainerManager {
35
47
  * writing through stale mounts while the runtime works in the new location.
36
48
  */
37
49
  removeContainersForConversations(conversationIds: ReadonlySet<string>): Promise<void>;
50
+ /**
51
+ * Arm the office-layout container migration. Containers whose binds still
52
+ * reference legacy raw-id paths are moved to the office-key layout without
53
+ * losing their writable layer: commit → remove → create with translated
54
+ * binds, preserving everything installed inside. Idempotent per container
55
+ * and resumable — the pre-removal binds ride on the snapshot image label,
56
+ * so a crash between commit and create resumes from the snapshot.
57
+ */
58
+ armContainerLayoutMigration(translator: ContainerBindTranslator): void;
59
+ /**
60
+ * Sweep every managed container through the layout migration, serially and
61
+ * with a small gap, so the one-time cost never lands on the boot path or on
62
+ * a message. Safe to fire-and-forget; each unit is idempotent.
63
+ */
64
+ sweepContainerLayoutMigration(delayMs?: number): Promise<void>;
65
+ /** Move one container to the office-key layout; no-op when already there. */
66
+ private migrateContainerLayout;
67
+ private migrateContainerLayoutInner;
68
+ /** Binds recorded on a snapshot image, or undefined when no snapshot exists. */
69
+ private readMigrateImageBinds;
70
+ private readImageLabel;
71
+ /**
72
+ * Delete a container's layout snapshot after an intentional removal, so a
73
+ * missing container plus a surviving snapshot can only mean a crash in the
74
+ * middle of the layout migration (the resume discriminator).
75
+ */
76
+ private removeMigrateSnapshot;
77
+ private listMigrateSnapshotContainerNames;
78
+ private removeDanglingMigrateImages;
38
79
  reconcile(): Promise<void>;
39
80
  private setState;
40
81
  private getContainerName;
@@ -1 +1 @@
1
- {"version":3,"file":"provisioner.d.ts","sourceRoot":"","sources":["../src/provisioner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provisioner.d.ts","sourceRoot":"","sources":["../src/provisioner.ts"],"names":[],"mappings":"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"}
@@ -19,16 +19,30 @@ function isDockerNotFoundError(err) {
19
19
  /network [^\n]+ not found/.test(haystack) ||
20
20
  /error: no such [^\n]+/.test(haystack));
21
21
  }
22
+ /** Parse `src:dst[:ro]` back into a ContainerMount for signature computation. */
23
+ function bindSpecToMount(bindSpec) {
24
+ const readOnly = bindSpec.endsWith(":ro");
25
+ const spec = readOnly ? bindSpec.slice(0, -3) : bindSpec;
26
+ const separator = spec.indexOf(":");
27
+ return {
28
+ source: spec.slice(0, separator),
29
+ target: spec.slice(separator + 1),
30
+ ...(readOnly ? { readOnly: true } : {}),
31
+ };
32
+ }
22
33
  export class DockerContainerManager {
23
34
  static { this.MANAGED_LABEL = "mikan.managed=true"; }
24
35
  static { this.IMAGE_MODE_LABEL = "mikan.sandbox=image"; }
25
36
  static { this.VAULT_ID_LABEL_KEY = "mikan.vault-id"; }
26
37
  static { this.CONVERSATION_ID_LABEL_KEY = "mikan.conversation-id"; }
27
38
  static { this.MOUNT_SIGNATURE_LABEL_KEY = "mikan.mount-signature"; }
39
+ static { this.MIGRATE_IMAGE_PREFIX = "mikan-migrate"; }
40
+ static { this.MIGRATE_BINDS_LABEL_KEY = "mikan.migrate-binds"; }
28
41
  constructor(image, options = {}) {
29
42
  this.image = image;
30
43
  this.state = new Map();
31
44
  this.inflight = new Map();
45
+ this.layoutMigrations = new Map();
32
46
  this.boostedKeys = new Set();
33
47
  this.overrideLimits = new Map();
34
48
  this.limits = options.limits;
@@ -57,12 +71,17 @@ export class DockerContainerManager {
57
71
  async provisionInner(containerKey, options) {
58
72
  const containerName = options.containerName ?? DockerContainerManager.containerName(containerKey);
59
73
  const mounts = options.mounts ?? [];
74
+ // A container still on the pre-office layout must move its writable layer
75
+ // to the new mounts before the drift check below would recreate it from
76
+ // the base image and discard everything installed inside.
77
+ await this.migrateContainerLayout(containerName);
60
78
  const status = await this.inspectStatus(containerName);
61
79
  try {
62
80
  if (status !== "missing" &&
63
81
  (await this.hasRuntimeDrift(containerKey, containerName, mounts))) {
64
82
  log.logInfo(`Container ${containerName} configuration changed; recreating container`);
65
83
  await this.execFileImpl("docker", ["rm", "-f", containerName]);
84
+ await this.removeMigrateSnapshot(containerName);
66
85
  await this.runContainer(containerKey, containerName, mounts, options);
67
86
  log.logInfo(`Container ${containerName} recreated`);
68
87
  }
@@ -141,6 +160,7 @@ export class DockerContainerManager {
141
160
  catch (err) {
142
161
  log.logWarning(`Failed to remove network ${networkName}`, err instanceof Error ? err.message : String(err));
143
162
  }
163
+ await this.removeMigrateSnapshot(containerName);
144
164
  this.state.delete(containerKey);
145
165
  this.boostedKeys.delete(containerKey);
146
166
  this.overrideLimits.delete(containerKey);
@@ -172,6 +192,225 @@ export class DockerContainerManager {
172
192
  await this.forceRemoveContainer(containerName, `Removed container ${containerName} after office migration`, `Failed to remove container ${containerName} after office migration`);
173
193
  }));
174
194
  }
195
+ /**
196
+ * Arm the office-layout container migration. Containers whose binds still
197
+ * reference legacy raw-id paths are moved to the office-key layout without
198
+ * losing their writable layer: commit → remove → create with translated
199
+ * binds, preserving everything installed inside. Idempotent per container
200
+ * and resumable — the pre-removal binds ride on the snapshot image label,
201
+ * so a crash between commit and create resumes from the snapshot.
202
+ */
203
+ armContainerLayoutMigration(translator) {
204
+ this.bindTranslator = translator;
205
+ }
206
+ /**
207
+ * Sweep every managed container through the layout migration, serially and
208
+ * with a small gap, so the one-time cost never lands on the boot path or on
209
+ * a message. Safe to fire-and-forget; each unit is idempotent.
210
+ */
211
+ async sweepContainerLayoutMigration(delayMs = 2000) {
212
+ if (!this.bindTranslator)
213
+ return;
214
+ let names;
215
+ try {
216
+ // Include snapshot names: a crash between commit and create leaves a
217
+ // snapshot with no container, which must resume before the GC below
218
+ // could mistake it for an orphan.
219
+ const containers = await this.listContainerNamesByLabel();
220
+ const snapshots = await this.listMigrateSnapshotContainerNames();
221
+ names = Array.from(new Set([...containers, ...snapshots]));
222
+ }
223
+ catch (err) {
224
+ log.logWarning("Container layout sweep could not list containers", String(err));
225
+ return;
226
+ }
227
+ for (const containerName of names) {
228
+ try {
229
+ await this.migrateContainerLayout(containerName);
230
+ }
231
+ catch (err) {
232
+ log.logWarning(`Container layout migration failed for ${containerName}; leaving it in place`, err instanceof Error ? err.message : String(err));
233
+ }
234
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
235
+ }
236
+ // Snapshot images whose container has since been recreated from the base
237
+ // image are no longer referenced; reclaim them.
238
+ await this.removeDanglingMigrateImages();
239
+ }
240
+ /** Move one container to the office-key layout; no-op when already there. */
241
+ migrateContainerLayout(containerName) {
242
+ const translator = this.bindTranslator;
243
+ if (!translator)
244
+ return Promise.resolve();
245
+ const existing = this.layoutMigrations.get(containerName);
246
+ if (existing)
247
+ return existing;
248
+ const pending = this.migrateContainerLayoutInner(containerName, translator).finally(() => {
249
+ this.layoutMigrations.delete(containerName);
250
+ });
251
+ this.layoutMigrations.set(containerName, pending);
252
+ return pending;
253
+ }
254
+ async migrateContainerLayoutInner(containerName, translator) {
255
+ const snapshotImage = `${DockerContainerManager.MIGRATE_IMAGE_PREFIX}:${containerName}`;
256
+ let originalBinds;
257
+ const status = await this.inspectStatus(containerName);
258
+ if (status === "missing") {
259
+ // Resume path: a previous run committed and removed the container but
260
+ // died before creating the replacement. The snapshot carries the
261
+ // pre-removal binds on its label.
262
+ const labeled = await this.readMigrateImageBinds(snapshotImage);
263
+ if (labeled === undefined)
264
+ return;
265
+ originalBinds = labeled;
266
+ }
267
+ else {
268
+ originalBinds = await this.inspectBindMounts(containerName);
269
+ const translated = originalBinds.map(translator);
270
+ if (this.sameBinds(translated.toSorted(), originalBinds.slice().toSorted())) {
271
+ return; // already on the office layout
272
+ }
273
+ log.logInfo(`Migrating container ${containerName} to the office layout`);
274
+ await this.execFileImpl("docker", [
275
+ "commit",
276
+ "-c",
277
+ `LABEL ${DockerContainerManager.MIGRATE_BINDS_LABEL_KEY}=${JSON.stringify(JSON.stringify(originalBinds))}`,
278
+ containerName,
279
+ snapshotImage,
280
+ ]);
281
+ await this.execFileImpl("docker", ["rm", "-f", containerName]);
282
+ }
283
+ const translated = originalBinds.map(translator);
284
+ const containerKey = this.containerKeyFromContainerName(containerName) ?? containerName;
285
+ const networkName = await this.ensureNetwork(containerKey);
286
+ const conversationId = await this.readImageLabel(snapshotImage, DockerContainerManager.CONVERSATION_ID_LABEL_KEY);
287
+ const labels = [
288
+ "--label",
289
+ DockerContainerManager.MANAGED_LABEL,
290
+ "--label",
291
+ DockerContainerManager.IMAGE_MODE_LABEL,
292
+ "--label",
293
+ `${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,
294
+ ];
295
+ if (conversationId) {
296
+ labels.push("--label", `${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}=${conversationId}`);
297
+ }
298
+ labels.push("--label", `${DockerContainerManager.MOUNT_SIGNATURE_LABEL_KEY}=${this.mountSignature(translated.map(bindSpecToMount))}`);
299
+ // `create`, not `run`: idle containers stay stopped exactly as they were,
300
+ // and the normal provision path starts them on the next message.
301
+ await this.execFileImpl("docker", [
302
+ "create",
303
+ "--name",
304
+ containerName,
305
+ "--network",
306
+ networkName,
307
+ "--cap-drop",
308
+ "ALL",
309
+ "--security-opt",
310
+ "no-new-privileges",
311
+ "--pids-limit",
312
+ "1024",
313
+ ...labels,
314
+ ...this.resourceLimitArgs(this.effectiveLimits(containerKey)),
315
+ ...translated.flatMap((bind) => ["-v", bind]),
316
+ snapshotImage,
317
+ "sleep",
318
+ "infinity",
319
+ ]);
320
+ log.logInfo(`Container ${containerName} migrated to the office layout`);
321
+ }
322
+ /** Binds recorded on a snapshot image, or undefined when no snapshot exists. */
323
+ async readMigrateImageBinds(snapshotImage) {
324
+ const raw = await this.readImageLabel(snapshotImage, DockerContainerManager.MIGRATE_BINDS_LABEL_KEY);
325
+ if (raw === undefined)
326
+ return undefined;
327
+ const parsed = JSON.parse(raw);
328
+ if (!Array.isArray(parsed) || parsed.some((bind) => typeof bind !== "string")) {
329
+ throw new Error(`Snapshot image ${snapshotImage} carries malformed migrate binds`);
330
+ }
331
+ return parsed;
332
+ }
333
+ async readImageLabel(image, key) {
334
+ try {
335
+ const { stdout } = await this.execFileImpl("docker", [
336
+ "inspect",
337
+ "-f",
338
+ `{{index .Config.Labels "${key}"}}`,
339
+ image,
340
+ ]);
341
+ return this.normalizeDockerValue(stdout.trim());
342
+ }
343
+ catch {
344
+ return undefined;
345
+ }
346
+ }
347
+ /**
348
+ * Delete a container's layout snapshot after an intentional removal, so a
349
+ * missing container plus a surviving snapshot can only mean a crash in the
350
+ * middle of the layout migration (the resume discriminator).
351
+ */
352
+ async removeMigrateSnapshot(containerName) {
353
+ try {
354
+ await this.execFileImpl("docker", [
355
+ "rmi",
356
+ `${DockerContainerManager.MIGRATE_IMAGE_PREFIX}:${containerName}`,
357
+ ]);
358
+ }
359
+ catch {
360
+ // No snapshot for this container — the common case.
361
+ }
362
+ }
363
+ async listMigrateSnapshotContainerNames() {
364
+ try {
365
+ const { stdout } = await this.execFileImpl("docker", [
366
+ "images",
367
+ DockerContainerManager.MIGRATE_IMAGE_PREFIX,
368
+ "--format",
369
+ "{{.Tag}}",
370
+ ]);
371
+ return this.parseNameLines(stdout);
372
+ }
373
+ catch {
374
+ return [];
375
+ }
376
+ }
377
+ async removeDanglingMigrateImages() {
378
+ let stdout;
379
+ try {
380
+ ({ stdout } = await this.execFileImpl("docker", [
381
+ "images",
382
+ `${DockerContainerManager.MIGRATE_IMAGE_PREFIX}`,
383
+ "--format",
384
+ "{{.Repository}}:{{.Tag}}",
385
+ ]));
386
+ }
387
+ catch {
388
+ return;
389
+ }
390
+ for (const image of this.parseNameLines(stdout)) {
391
+ const containerName = image.slice(DockerContainerManager.MIGRATE_IMAGE_PREFIX.length + 1);
392
+ const status = await this.inspectStatus(containerName);
393
+ if (status !== "missing") {
394
+ // Still referenced (until the container's next natural recreation
395
+ // from the base image); keep the snapshot.
396
+ const { stdout: imageRef } = await this.execFileImpl("docker", [
397
+ "inspect",
398
+ "-f",
399
+ "{{.Config.Image}}",
400
+ containerName,
401
+ ]);
402
+ if (imageRef.trim() === image)
403
+ continue;
404
+ }
405
+ try {
406
+ await this.execFileImpl("docker", ["rmi", image]);
407
+ log.logInfo(`Removed layout-migration snapshot image ${image}`);
408
+ }
409
+ catch (err) {
410
+ log.logWarning(`Could not remove snapshot image ${image}`, String(err));
411
+ }
412
+ }
413
+ }
175
414
  async reconcile() {
176
415
  const discovered = new Set();
177
416
  const labeledNames = await this.listContainerNamesByLabel();