@cortexkit/aft-opencode 0.18.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/logger.d.ts CHANGED
@@ -7,8 +7,8 @@ export declare function error(message: string, data?: unknown): void;
7
7
  * a session's tool call). Bridge-lifecycle logs (spawn, version, idle) are
8
8
  * project-scoped, not session-scoped — use `log`/`warn`/`error` for those.
9
9
  */
10
- export declare function sessionLog(sessionId: string, message: string, data?: unknown): void;
11
- export declare function sessionWarn(sessionId: string, message: string, data?: unknown): void;
12
- export declare function sessionError(sessionId: string, message: string, data?: unknown): void;
10
+ export declare function sessionLog(sessionId: string | undefined, message: string, data?: unknown): void;
11
+ export declare function sessionWarn(sessionId: string | undefined, message: string, data?: unknown): void;
12
+ export declare function sessionError(sessionId: string | undefined, message: string, data?: unknown): void;
13
13
  export declare function getLogFilePath(): string;
14
14
  //# sourceMappingURL=logger.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAkEA,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAEzD;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE1D;AAED,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAEnF;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAEpF;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAErF;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAuEA,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAEzD;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE1D;AAED,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAE/F;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAEhG;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAEjG;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
package/dist/lsp.d.ts CHANGED
@@ -27,6 +27,6 @@ export declare const LSP_SYMBOL_KIND_MAP: Record<number, string>;
27
27
  * Failures are silent at the caller level — the binary falls back to
28
28
  * tree-sitter-only disambiguation when `lsp_hints` is absent.
29
29
  */
30
- export declare function queryLspHints(client: PluginInput["client"], symbolName: string, directory?: string): Promise<LspHints | undefined>;
30
+ export declare function queryLspHints(client: PluginInput["client"], symbolName: string, directory?: string, sessionId?: string): Promise<LspHints | undefined>;
31
31
  export {};
32
32
  //# sourceMappingURL=lsp.d.ts.map
package/dist/lsp.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"lsp.d.ts","sourceRoot":"","sources":["../src/lsp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD,mEAAmE;AACnE,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,8DAA8D;AAC9D,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOtD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,EAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAwD/B"}
1
+ {"version":3,"file":"lsp.d.ts","sourceRoot":"","sources":["../src/lsp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD,mEAAmE;AACnE,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,8DAA8D;AAC9D,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOtD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,EAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAwD/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAiOH,MAAM,WAAW,mBAAmB;IAClC,8BAA8B;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,yBAAyB,GAAG,uBAAuB,GAAG,oBAAoB,CAAC;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW3F;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0C1F;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAAE,EAClB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAqCf;AAwED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,uBAAuB,EAC7B,QAAQ,EAAE,gBAAgB,EAAE,GAC3B,OAAO,CAAC,IAAI,CAAC,CAwBf;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0C9E"}
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA0OH,MAAM,WAAW,mBAAmB;IAClC,8BAA8B;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,yBAAyB,GAAG,uBAAuB,GAAG,oBAAoB,CAAC;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW3F;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0C1F;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAAE,EAClB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAqCf;AAwED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,uBAAuB,EAC7B,QAAQ,EAAE,gBAAgB,EAAE,GAC3B,OAAO,CAAC,IAAI,CAAC,CAwBf;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0C9E"}
package/dist/pool.d.ts CHANGED
@@ -38,6 +38,8 @@ export declare class BridgePool {
38
38
  * cold-start cost on a cheap query.
39
39
  */
40
40
  getAnyActiveBridge(projectRoot: string): BinaryBridge | null;
41
+ /** Get an alive bridge only when it belongs to the requested project root. */
42
+ getActiveBridgeForRoot(projectRoot: string): BinaryBridge | null;
41
43
  /**
42
44
  * Get or create the bridge for `projectRoot`.
43
45
  *
@@ -1 +1 @@
1
- {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAY5D,MAAM,WAAW,WAAY,SAAQ,aAAa;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,UAAU;IACrB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA0B;IAC1D,OAAO,CAAC,YAAY,CAA+C;gBAGjE,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,WAAgB,EACzB,eAAe,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAoB/C;;;;;;;OAOG;IACH,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAgB5D;;;;;;;OAOG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY;IAmB5C,wEAAwE;IACxE,OAAO,CAAC,OAAO;IAUf,yDAAyD;IACzD,OAAO,CAAC,QAAQ;IAgBhB,wDAAwD;IAClD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAU/B;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcnD,4CAA4C;IAC5C,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAY5D,MAAM,WAAW,WAAY,SAAQ,aAAa;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,UAAU;IACrB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA0B;IAC1D,OAAO,CAAC,YAAY,CAA+C;gBAGjE,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,WAAgB,EACzB,eAAe,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAqB/C;;;;;;;OAOG;IACH,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAgB5D,8EAA8E;IAC9E,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAQhE;;;;;;;OAOG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY;IAmB5C,wEAAwE;IACxE,OAAO,CAAC,OAAO;IAUf,yDAAyD;IACzD,OAAO,CAAC,QAAQ;IAgBhB,wDAAwD;IAClD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAU/B;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcnD,4CAA4C;IAC5C,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Cache-busting fix: every PromptInput we send to OpenCode starts a new
3
+ * assistant turn (via `noReply: false`) or stores a new user message
4
+ * (via `noReply: true`). OpenCode's `createUserMessage` (in
5
+ * `packages/opencode/src/session/prompt.ts`) only preserves the user's
6
+ * model variant if we pass it explicitly — otherwise it falls back to
7
+ * `agent.variant` or undefined, silently switching the next assistant
8
+ * turn off the variant the human chose. That switch evicts the
9
+ * provider's prefix cache on every notification.
10
+ *
11
+ * This module fetches the last real user message's `{ providerID,
12
+ * modelID, variant }` and exposes it so prompt callers can include it
13
+ * verbatim in `PromptInput.model` + `PromptInput.variant`. Results are
14
+ * memoised per session for a short window so a batch of warnings or
15
+ * bg-completions only hits the messages API once.
16
+ */
17
+ export interface LastUserModel {
18
+ providerID: string;
19
+ modelID: string;
20
+ variant?: string;
21
+ }
22
+ /**
23
+ * Returns the most recent real user message's model + variant for the
24
+ * session, or `null` when the session has no user messages or the
25
+ * client doesn't expose `session.messages`. Result is cached for
26
+ * {@link CACHE_TTL_MS} ms per session — long enough to coalesce a
27
+ * batch of notifications, short enough that an actual variant change
28
+ * inside the conversation is picked up promptly.
29
+ *
30
+ * NOTE: Skips synthetic user messages we just produced ourselves
31
+ * (those whose only parts are `ignored: true`). Otherwise our own
32
+ * announcement could pin the variant after a single round-trip and
33
+ * defeat the fix.
34
+ */
35
+ export declare function getLastUserModel(client: unknown, sessionId: string): Promise<LastUserModel | null>;
36
+ /** Test-only: drop the cache so unit tests can simulate fresh sessions. */
37
+ export declare function __resetLastUserModelCacheForTests(): void;
38
+ //# sourceMappingURL=last-user-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"last-user-model.d.ts","sourceRoot":"","sources":["../../src/shared/last-user-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAwBH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA0C/B;AAiBD,2EAA2E;AAC3E,wBAAgB,iCAAiC,IAAI,IAAI,CAExD"}
@@ -6,6 +6,8 @@ export declare class AftRpcClient {
6
6
  constructor(storageDir: string, directory: string);
7
7
  /** Call an RPC method. Retries port resolution if the server isn't ready yet. */
8
8
  call<T = Record<string, unknown>>(method: string, params?: Record<string, unknown>): Promise<T>;
9
+ private callResolved;
10
+ private retryAfterReset;
9
11
  /** Check if the RPC server is reachable. */
10
12
  isAvailable(): Promise<boolean>;
11
13
  private resolvePort;
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAQA,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAS;gBAElB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAIjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,CAAC,CAAC;IAoBb,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;YASvB,WAAW;YAIX,eAAe;IAoC7B,OAAO,CAAC,YAAY;YAuBN,WAAW;YAWX,gBAAgB;IAU9B,KAAK,IAAI,IAAI;CAKd"}
1
+ {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAQA,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAS;gBAElB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAIjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,CAAC,CAAC;YASC,YAAY;YA6BZ,eAAe;IAe7B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;YASvB,WAAW;YAIX,eAAe;IAoC7B,OAAO,CAAC,YAAY;YAuBN,WAAW;YAWX,gBAAgB;IAU9B,KAAK,IAAI,IAAI;CAKd"}
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-server.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-server.ts"],"names":[],"mappings":"AAOA,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAExF,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,YAAY,CAAS;gBAEjB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAIjD,sCAAsC;IACtC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IAIjD,6DAA6D;IACvD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAuC9B,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAaZ,OAAO,CAAC,QAAQ;CAmEjB"}
1
+ {"version":3,"file":"rpc-server.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-server.ts"],"names":[],"mappings":"AAOA,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAExF,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,YAAY,CAAS;gBAEjB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAIjD,sCAAsC;IACtC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IAIjD,6DAA6D;IACvD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAuC9B,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAaZ,OAAO,CAAC,QAAQ;CAoEjB"}
@@ -1,6 +1,22 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { type Dispatcher } from "undici";
1
3
  interface FetchUrlOptions {
2
4
  allowPrivate?: boolean;
5
+ /** Test-only injection point. Production fetches use undici with DNS pinning. */
6
+ fetchImpl?: FetchImpl;
7
+ /** Test-only injection point. Production DNS resolution uses node:dns/promises.lookup. */
8
+ lookup?: LookupFn;
9
+ /** Test-only injection point for observing the DNS-pinned dispatcher. */
10
+ dispatcherFactory?: (validatedIp: string) => Dispatcher;
3
11
  }
12
+ type LookupFn = typeof lookup;
13
+ interface FetchInit {
14
+ signal?: AbortSignal;
15
+ redirect?: "manual";
16
+ dispatcher?: Dispatcher;
17
+ headers?: Record<string, string>;
18
+ }
19
+ type FetchImpl = (input: string, init: FetchInit) => Promise<Response>;
4
20
  /** Exported for unit tests. */
5
21
  export declare function _isPrivateIpv4(address: string): boolean;
6
22
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"url-fetch.d.ts","sourceRoot":"","sources":["../../src/shared/url-fetch.ts"],"names":[],"mappings":"AAsBA,UAAU,eAAe;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAyBD,+BAA+B;AAC/B,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAavD;AAoND;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAgGjB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAkCxD"}
1
+ {"version":3,"file":"url-fetch.d.ts","sourceRoot":"","sources":["../../src/shared/url-fetch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAW3C,OAAO,EAAS,KAAK,UAAU,EAAwB,MAAM,QAAQ,CAAC;AAWtE,UAAU,eAAe;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iFAAiF;IACjF,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,UAAU,CAAC;CACzD;AAED,KAAK,QAAQ,GAAG,OAAO,MAAM,CAAC;AAC9B,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAyBvE,+BAA+B;AAC/B,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAavD;AAgPD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAgGjB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAkCxD"}
@@ -1,4 +1,6 @@
1
1
  import type { ToolDefinition } from "@opencode-ai/plugin";
2
2
  import type { PluginContext } from "../types.js";
3
3
  export declare function createBashTool(ctx: PluginContext): ToolDefinition;
4
+ export declare function createBashStatusTool(ctx: PluginContext): ToolDefinition;
5
+ export declare function createBashKillTool(ctx: PluginContext): ToolDefinition;
4
6
  //# sourceMappingURL=bash.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAKvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAsDjD,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CA8IjE"}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAKvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAsDjD,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAoJjE;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CA4BvE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAmBrE"}
@@ -1 +1 @@
1
- {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAkEjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AA+QtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAgJjE;AA08BD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAU/E;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAmEnF"}
1
+ {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAkEjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AA+QtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAgJjE;AAm+BD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAY/E;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAmEnF"}
@@ -1,7 +1,21 @@
1
1
  import type { ToolDefinition } from "@opencode-ai/plugin";
2
2
  import type { PluginContext } from "../types.js";
3
+ interface ZoomBatchSymbolResult {
4
+ name: string;
5
+ success: boolean;
6
+ content?: string;
7
+ error?: string;
8
+ }
9
+ interface ZoomBatchResult {
10
+ complete: boolean;
11
+ symbols: ZoomBatchSymbolResult[];
12
+ text: string;
13
+ }
3
14
  /**
4
15
  * Tool definitions for code reading commands: outline + zoom.
5
16
  */
6
17
  export declare function readingTools(ctx: PluginContext): Record<string, ToolDefinition>;
18
+ /** Exported for regression tests. */
19
+ export declare function formatZoomBatchResult(symbols: string[], responses: Record<string, unknown>[]): ZoomBatchResult;
20
+ export {};
7
21
  //# sourceMappingURL=reading.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reading.d.ts","sourceRoot":"","sources":["../../src/tools/reading.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA2CjD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAuK/E"}
1
+ {"version":3,"file":"reading.d.ts","sourceRoot":"","sources":["../../src/tools/reading.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA2CjD,UAAU,qBAAqB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,eAAe;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0K/E;AAED,qCAAqC;AACrC,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EAAE,EACjB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACnC,eAAe,CA0BjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"refactoring.d.ts","sourceRoot":"","sources":["../../src/tools/refactoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAajD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA2HnF"}
1
+ {"version":3,"file":"refactoring.d.ts","sourceRoot":"","sources":["../../src/tools/refactoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAajD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAgInF"}
package/dist/tui.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@cortexkit/aft-opencode",
5
- version: "0.18.2",
5
+ version: "0.18.4",
6
6
  type: "module",
7
7
  description: "OpenCode plugin for Agent File Tools (AFT) \u2014 tree-sitter and lsp powered code analysis",
8
8
  main: "dist/index.js",
@@ -25,6 +25,7 @@ var package_default = {
25
25
  pretest: "cd ../.. && cargo build",
26
26
  test: "bun test",
27
27
  "test:e2e": "bun test src/__tests__/e2e/",
28
+ lint: "biome check src",
28
29
  prepublishOnly: "bun run build"
29
30
  },
30
31
  dependencies: {
@@ -32,14 +33,15 @@ var package_default = {
32
33
  "@opencode-ai/plugin": "^1.2.26",
33
34
  "@opencode-ai/sdk": "^1.2.26",
34
35
  "comment-json": "^4.6.2",
36
+ undici: "^7.25.0",
35
37
  zod: "^4.1.8"
36
38
  },
37
39
  optionalDependencies: {
38
- "@cortexkit/aft-darwin-arm64": "0.18.2",
39
- "@cortexkit/aft-darwin-x64": "0.18.2",
40
- "@cortexkit/aft-linux-arm64": "0.18.2",
41
- "@cortexkit/aft-linux-x64": "0.18.2",
42
- "@cortexkit/aft-win32-x64": "0.18.2"
40
+ "@cortexkit/aft-darwin-arm64": "0.18.4",
41
+ "@cortexkit/aft-darwin-x64": "0.18.4",
42
+ "@cortexkit/aft-linux-arm64": "0.18.4",
43
+ "@cortexkit/aft-linux-x64": "0.18.4",
44
+ "@cortexkit/aft-win32-x64": "0.18.4"
43
45
  },
44
46
  devDependencies: {
45
47
  "@types/node": "^22.0.0",
@@ -72,7 +74,8 @@ import * as fs from "fs";
72
74
  import * as os from "os";
73
75
  import * as path from "path";
74
76
  var TAG = "[aft-plugin]";
75
- var logFile = path.join(os.tmpdir(), "aft-plugin.log");
77
+ var isTestEnv = process.env.BUN_TEST === "1" || false;
78
+ var logFile = path.join(os.tmpdir(), isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
76
79
  var useStderr = process.env.AFT_LOG_STDERR === "1";
77
80
  var buffer = [];
78
81
  var flushTimer = null;
@@ -155,17 +158,40 @@ class AftRpcClient {
155
158
  if (!info) {
156
159
  throw new Error("AFT RPC server not available");
157
160
  }
158
- const response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
159
- method: "POST",
160
- headers: { "Content-Type": "application/json" },
161
- body: JSON.stringify({ ...params, token: info.token })
162
- });
161
+ return this.callResolved(method, params, info, true);
162
+ }
163
+ async callResolved(method, params, info, retryOnConnectionFailure) {
164
+ let response;
165
+ try {
166
+ response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
167
+ method: "POST",
168
+ headers: { "Content-Type": "application/json" },
169
+ body: JSON.stringify({ ...params, token: info.token })
170
+ });
171
+ } catch (err) {
172
+ if (!retryOnConnectionFailure)
173
+ throw err;
174
+ return this.retryAfterReset(method, params, err);
175
+ }
163
176
  if (!response.ok) {
164
177
  const text = await response.text();
178
+ if (response.status >= 500 && retryOnConnectionFailure) {
179
+ return this.retryAfterReset(method, params, `HTTP ${response.status}: ${text}`);
180
+ }
165
181
  throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
166
182
  }
167
183
  return await response.json();
168
184
  }
185
+ async retryAfterReset(method, params, reason) {
186
+ const message = reason instanceof Error ? reason.message : String(reason);
187
+ warn(`RPC ${method} failed on cached port; retrying after port refresh (${message})`);
188
+ this.reset();
189
+ const refreshed = await this.resolvePortInfo();
190
+ if (!refreshed) {
191
+ throw new Error("AFT RPC server not available after port refresh");
192
+ }
193
+ return this.callResolved(method, params, refreshed, false);
194
+ }
169
195
  async isAvailable() {
170
196
  try {
171
197
  const port = await this.resolvePort();
@@ -0,0 +1,25 @@
1
+ import type { AftConfig } from "./config.js";
2
+ export interface WorkflowHintsOpts {
3
+ /** `tool_surface` setting — controls which tools are registered. */
4
+ toolSurface: "minimal" | "recommended" | "all";
5
+ /** `hoist_builtin_tools` setting — affects tool name (read vs aft_read). */
6
+ hoistBuiltins: boolean;
7
+ /** `experimental.semantic_search` — gates `aft_search` mention. */
8
+ semanticEnabled: boolean;
9
+ /** `experimental.bash.background` — gates background-bash paragraph. */
10
+ bashBackgroundEnabled: boolean;
11
+ /** Set of disabled tool names (after surface filtering). */
12
+ disabledTools: Set<string>;
13
+ }
14
+ /**
15
+ * Build the workflow hints block. Returns `null` when no hints are
16
+ * applicable for the configured surface (e.g. `tool_surface: "minimal"`
17
+ * with no aft_outline/aft_zoom available — only safety tool is registered).
18
+ */
19
+ export declare function buildWorkflowHints(opts: WorkflowHintsOpts): string | null;
20
+ /**
21
+ * Resolve workflow-hints opts from a loaded AftConfig and the active
22
+ * disabled-tools set computed at registration time.
23
+ */
24
+ export declare function buildHintsFromConfig(config: AftConfig, disabledTools: Set<string>): string | null;
25
+ //# sourceMappingURL=workflow-hints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,WAAW,EAAE,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAoEzE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAQjG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
6
6
  "main": "dist/index.js",
@@ -23,6 +23,7 @@
23
23
  "pretest": "cd ../.. && cargo build",
24
24
  "test": "bun test",
25
25
  "test:e2e": "bun test src/__tests__/e2e/",
26
+ "lint": "biome check src",
26
27
  "prepublishOnly": "bun run build"
27
28
  },
28
29
  "dependencies": {
@@ -30,14 +31,15 @@
30
31
  "@opencode-ai/plugin": "^1.2.26",
31
32
  "@opencode-ai/sdk": "^1.2.26",
32
33
  "comment-json": "^4.6.2",
34
+ "undici": "^7.25.0",
33
35
  "zod": "^4.1.8"
34
36
  },
35
37
  "optionalDependencies": {
36
- "@cortexkit/aft-darwin-arm64": "0.18.2",
37
- "@cortexkit/aft-darwin-x64": "0.18.2",
38
- "@cortexkit/aft-linux-arm64": "0.18.2",
39
- "@cortexkit/aft-linux-x64": "0.18.2",
40
- "@cortexkit/aft-win32-x64": "0.18.2"
38
+ "@cortexkit/aft-darwin-arm64": "0.18.4",
39
+ "@cortexkit/aft-darwin-x64": "0.18.4",
40
+ "@cortexkit/aft-linux-arm64": "0.18.4",
41
+ "@cortexkit/aft-linux-x64": "0.18.4",
42
+ "@cortexkit/aft-win32-x64": "0.18.4"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@types/node": "^22.0.0",
package/src/logger.ts CHANGED
@@ -3,7 +3,12 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
 
5
5
  const TAG = "[aft-plugin]";
6
- const logFile = path.join(os.tmpdir(), "aft-plugin.log");
6
+
7
+ // Route test runs to a separate log file so `bun test` never pollutes the
8
+ // live session log that users read to diagnose problems. Bun sets BUN_TEST=1
9
+ // automatically; NODE_ENV=test covers other test harnesses.
10
+ const isTestEnv = process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
11
+ const logFile = path.join(os.tmpdir(), isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
7
12
 
8
13
  /**
9
14
  * When AFT_LOG_STDERR=1, logs go to stderr (useful for subprocess tests that
@@ -82,15 +87,15 @@ export function error(message: string, data?: unknown): void {
82
87
  * a session's tool call). Bridge-lifecycle logs (spawn, version, idle) are
83
88
  * project-scoped, not session-scoped — use `log`/`warn`/`error` for those.
84
89
  */
85
- export function sessionLog(sessionId: string, message: string, data?: unknown): void {
90
+ export function sessionLog(sessionId: string | undefined, message: string, data?: unknown): void {
86
91
  write("INFO", message, data, sessionId);
87
92
  }
88
93
 
89
- export function sessionWarn(sessionId: string, message: string, data?: unknown): void {
94
+ export function sessionWarn(sessionId: string | undefined, message: string, data?: unknown): void {
90
95
  write("WARN", message, data, sessionId);
91
96
  }
92
97
 
93
- export function sessionError(sessionId: string, message: string, data?: unknown): void {
98
+ export function sessionError(sessionId: string | undefined, message: string, data?: unknown): void {
94
99
  write("ERROR", message, data, sessionId);
95
100
  }
96
101
 
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Cache-busting fix: every PromptInput we send to OpenCode starts a new
3
+ * assistant turn (via `noReply: false`) or stores a new user message
4
+ * (via `noReply: true`). OpenCode's `createUserMessage` (in
5
+ * `packages/opencode/src/session/prompt.ts`) only preserves the user's
6
+ * model variant if we pass it explicitly — otherwise it falls back to
7
+ * `agent.variant` or undefined, silently switching the next assistant
8
+ * turn off the variant the human chose. That switch evicts the
9
+ * provider's prefix cache on every notification.
10
+ *
11
+ * This module fetches the last real user message's `{ providerID,
12
+ * modelID, variant }` and exposes it so prompt callers can include it
13
+ * verbatim in `PromptInput.model` + `PromptInput.variant`. Results are
14
+ * memoised per session for a short window so a batch of warnings or
15
+ * bg-completions only hits the messages API once.
16
+ */
17
+
18
+ interface SessionMessageInfo {
19
+ id?: string;
20
+ role?: string;
21
+ parts?: Array<{ ignored?: boolean }>;
22
+ model?: {
23
+ providerID?: string;
24
+ modelID?: string;
25
+ variant?: string;
26
+ };
27
+ }
28
+
29
+ interface SessionMessage {
30
+ info?: SessionMessageInfo;
31
+ parts?: Array<{ ignored?: boolean }>;
32
+ }
33
+
34
+ interface OpenCodeClientShape {
35
+ session?: {
36
+ messages?: (input: { path: { id: string } }) => Promise<{ data?: SessionMessage[] }>;
37
+ };
38
+ }
39
+
40
+ export interface LastUserModel {
41
+ providerID: string;
42
+ modelID: string;
43
+ variant?: string;
44
+ }
45
+
46
+ interface CacheEntry {
47
+ expiresAt: number;
48
+ model: LastUserModel | null;
49
+ }
50
+
51
+ const CACHE_TTL_MS = 5_000;
52
+ const CACHE_MAX_ENTRIES = 100;
53
+ const cache = new Map<string, CacheEntry>();
54
+
55
+ /**
56
+ * Returns the most recent real user message's model + variant for the
57
+ * session, or `null` when the session has no user messages or the
58
+ * client doesn't expose `session.messages`. Result is cached for
59
+ * {@link CACHE_TTL_MS} ms per session — long enough to coalesce a
60
+ * batch of notifications, short enough that an actual variant change
61
+ * inside the conversation is picked up promptly.
62
+ *
63
+ * NOTE: Skips synthetic user messages we just produced ourselves
64
+ * (those whose only parts are `ignored: true`). Otherwise our own
65
+ * announcement could pin the variant after a single round-trip and
66
+ * defeat the fix.
67
+ */
68
+ export async function getLastUserModel(
69
+ client: unknown,
70
+ sessionId: string,
71
+ ): Promise<LastUserModel | null> {
72
+ const now = Date.now();
73
+ const cached = cache.get(sessionId);
74
+ if (cached && cached.expiresAt > now) {
75
+ cache.delete(sessionId);
76
+ cache.set(sessionId, cached);
77
+ return cached.model;
78
+ }
79
+ if (cached) cache.delete(sessionId);
80
+
81
+ const c = client as OpenCodeClientShape;
82
+ const fetcher = c?.session?.messages;
83
+ if (typeof fetcher !== "function") {
84
+ return null;
85
+ }
86
+
87
+ let messages: SessionMessage[];
88
+ try {
89
+ const result = await fetcher({ path: { id: sessionId } });
90
+ messages = result?.data ?? [];
91
+ } catch {
92
+ // Don't cache failures — a transient API error shouldn't pin the
93
+ // session's variant resolution to "unknown" for 5 s.
94
+ return null;
95
+ }
96
+
97
+ for (let i = messages.length - 1; i >= 0; i--) {
98
+ const info = messages[i]?.info;
99
+ if (info?.role !== "user") continue;
100
+ if (isSyntheticIgnoredMessage(messages[i])) continue;
101
+ const model = info.model;
102
+ if (!model?.providerID || !model?.modelID) continue;
103
+ const resolved: LastUserModel = {
104
+ providerID: model.providerID,
105
+ modelID: model.modelID,
106
+ ...(model.variant ? { variant: model.variant } : {}),
107
+ };
108
+ setCache(sessionId, resolved);
109
+ return resolved;
110
+ }
111
+
112
+ return null;
113
+ }
114
+
115
+ function setCache(sessionId: string, model: LastUserModel | null): void {
116
+ if (cache.has(sessionId)) cache.delete(sessionId);
117
+ cache.set(sessionId, { expiresAt: Date.now() + CACHE_TTL_MS, model });
118
+ while (cache.size > CACHE_MAX_ENTRIES) {
119
+ const oldest = cache.keys().next().value;
120
+ if (typeof oldest !== "string") break;
121
+ cache.delete(oldest);
122
+ }
123
+ }
124
+
125
+ function isSyntheticIgnoredMessage(message: SessionMessage | undefined): boolean {
126
+ const parts = message?.info?.parts ?? message?.parts;
127
+ return Array.isArray(parts) && parts.length > 0 && parts.every((part) => part?.ignored === true);
128
+ }
129
+
130
+ /** Test-only: drop the cache so unit tests can simulate fresh sessions. */
131
+ export function __resetLastUserModelCacheForTests(): void {
132
+ cache.clear();
133
+ }
@@ -26,20 +26,53 @@ export class AftRpcClient {
26
26
  throw new Error("AFT RPC server not available");
27
27
  }
28
28
 
29
- const response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
30
- method: "POST",
31
- headers: { "Content-Type": "application/json" },
32
- body: JSON.stringify({ ...params, token: info.token }),
33
- });
29
+ return this.callResolved<T>(method, params, info, true);
30
+ }
31
+
32
+ private async callResolved<T>(
33
+ method: string,
34
+ params: Record<string, unknown>,
35
+ info: { port: number; token: string | null },
36
+ retryOnConnectionFailure: boolean,
37
+ ): Promise<T> {
38
+ let response: Response;
39
+ try {
40
+ response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
+ body: JSON.stringify({ ...params, token: info.token }),
44
+ });
45
+ } catch (err) {
46
+ if (!retryOnConnectionFailure) throw err;
47
+ return this.retryAfterReset<T>(method, params, err);
48
+ }
34
49
 
35
50
  if (!response.ok) {
36
51
  const text = await response.text();
52
+ if (response.status >= 500 && retryOnConnectionFailure) {
53
+ return this.retryAfterReset<T>(method, params, `HTTP ${response.status}: ${text}`);
54
+ }
37
55
  throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
38
56
  }
39
57
 
40
58
  return (await response.json()) as T;
41
59
  }
42
60
 
61
+ private async retryAfterReset<T>(
62
+ method: string,
63
+ params: Record<string, unknown>,
64
+ reason: unknown,
65
+ ): Promise<T> {
66
+ const message = reason instanceof Error ? reason.message : String(reason);
67
+ warn(`RPC ${method} failed on cached port; retrying after port refresh (${message})`);
68
+ this.reset();
69
+ const refreshed = await this.resolvePortInfo();
70
+ if (!refreshed) {
71
+ throw new Error("AFT RPC server not available after port refresh");
72
+ }
73
+ return this.callResolved<T>(method, params, refreshed, false);
74
+ }
75
+
43
76
  /** Check if the RPC server is reachable. */
44
77
  async isAvailable(): Promise<boolean> {
45
78
  try {
@@ -130,10 +130,11 @@ export class AftRpcServer {
130
130
 
131
131
  const { token: _token, ...handlerParams } = params;
132
132
 
133
- log(`RPC call: ${method} params=${JSON.stringify(handlerParams).slice(0, 200)}`);
133
+ // Successful RPC calls are intentionally NOT logged. The /aft-status
134
+ // TUI dialog polls every 1.5s while open, which used to spam the log
135
+ // with RPC call/result pairs. Errors are still logged (real signal).
134
136
  handler(handlerParams)
135
137
  .then((result) => {
136
- log(`RPC result: ${method} => ${JSON.stringify(result).slice(0, 200)}`);
137
138
  res.writeHead(200, { "Content-Type": "application/json" });
138
139
  res.end(JSON.stringify(result));
139
140
  })