فهرست منبع

fix(cli): harden launcher lifecycle

Turtle 2 ماه پیش
والد
کامیت
cd100a81f1
4فایلهای تغییر یافته به همراه19 افزوده شده و 21 حذف شده
  1. 9 17
      apps/cli/src/tui.ts
  2. 2 2
      packages/ui/app-boot/src/index.ts
  3. 5 1
      packages/ui/app-boot/tests/app-boot.spec.ts
  4. 3 1
      packages/ui/tui/src/index.ts

+ 9 - 17
apps/cli/src/tui.ts

@@ -144,23 +144,15 @@ export async function runTui(
   const entry = process.argv[1]
   const execve = process.execve?.bind(process)
   const app: { current?: Context } = {}
-  // Resuming reproduces THIS invocation with a different id. Meta mode is a
-  // subcommand that rejects `--config`, while the default surface carries it, so
-  // both the in-place handoff and the printed command derive from one shape.
-  // `meta` is only reproducible for a target inside this checkout: it chdirs to
-  // SOURCE_ROOT itself, which would override any other workspace, so a
-  // cross-workspace resume takes the default surface and the caller supplies the
-  // directory instead.
-  const resumeArgs = (sessionId: string, targetCwd?: string): string[] =>
-    workspace !== undefined && (targetCwd === undefined || targetCwd === workspace)
-      ? ['meta', `--resume=${sessionId}`]
-      : [
-        `--resume=${sessionId}`,
-        // Both config flags must survive the handoff: resuming into a different
-        // tree than the session was created in would silently change the agent.
-        ...resolvedConfig !== undefined ? ['--config', resolvedConfig] : [],
-        ...resolvedConfigReplace !== undefined ? ['--config-replace', resolvedConfigReplace] : [],
-      ]
+  // Resume always enters the default surface because meta rejects parent
+  // options, including `--resume`. The resumed session already persists its cwd.
+  const resumeArgs = (sessionId: string, _targetCwd?: string): string[] => [
+    `--resume=${sessionId}`,
+    // Both config flags must survive the handoff: resuming into a different
+    // tree than the session was created in would silently change the agent.
+    ...resolvedConfig !== undefined ? ['--config', resolvedConfig] : [],
+    ...resolvedConfigReplace !== undefined ? ['--config-replace', resolvedConfigReplace] : [],
+  ]
   // Mint the fresh id here rather than in the app bundle: the exit line names
   // the session to resume, so the launcher must know it before the tree boots.
   const identity: MainSessionIdentity = resumeSessionId === undefined

+ 2 - 2
packages/ui/app-boot/src/index.ts

@@ -208,7 +208,7 @@ export function assertEntriesLoaded(ctx: Context, binName: string): void {
  * (see {@link resolveConfigPath}).
  * @param patches - optional overlay patches applied over the included tree
  * (see {@link loadPersonalPatches}); an empty list mounts none.
- * @param prepare - optional host setup run against the root context before any Loader entry mounts.
+ * @param prepare - optional host setup run after Loader installation and before any config-tree entry mounts.
  * @returns the root context once every entry has started.
  */
 export async function boot(
@@ -218,10 +218,10 @@ export async function boot(
   prepare?: (ctx: Context) => Promise<void> | void,
 ): Promise<Context> {
   const ctx = new Context()
-  await prepare?.(ctx)
   ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + '/'
   await ctx.plugin(Loader)
   ctx.loader.builtins.include = Include
+  await prepare?.(ctx)
   await ctx.loader.create({
     name: 'cordis:include',
     config: {

+ 5 - 1
packages/ui/app-boot/tests/app-boot.spec.ts

@@ -195,7 +195,11 @@ describe('boot', () => {
     writeFileSync(join(dir, 'noop.mjs'), 'export const name = "noop"\nexport function apply() {}\n')
     writeFileSync(join(dir, 'cordis.yml'), '- id: noop\n  name: ./noop.mjs\n')
     const prepared: Context[] = []
-    const ctx = await boot(NAME, join(dir, 'cordis.yml'), undefined, (hostCtx) => { prepared.push(hostCtx) })
+    const ctx = await boot(NAME, join(dir, 'cordis.yml'), undefined, (hostCtx) => {
+      expect(hostCtx.loader).toBeDefined()
+      expect([...hostCtx.loader.entries()]).toEqual([])
+      prepared.push(hostCtx)
+    })
     try {
       expect(prepared).toEqual([ctx])
     } finally {

+ 3 - 1
packages/ui/tui/src/index.ts

@@ -1676,7 +1676,9 @@ export function apply(ctx: Context, config: Config): void {
     initialSkill === undefined ? {} : { initialSkill },
   ), {
     terminal: new ProcessTerminal(),
-    exit: code => process.exit(code),
+    exit: (code) => {
+      void ctx.fiber.dispose().finally(() => { process.exit(code) })
+    },
     ...resumeHost === undefined ? {} : { handoffResume: (sessionId, cwd) => resumeHost.handoff(sessionId, cwd) },
     ...goodbyeMessage === undefined ? {} : { goodbyeMessage },
   })