fix: adapt the bridge to omp 18.4.8 host differences
Validate / validate (push) Successful in 9m53s

- resolve the agent dir through the host getAgentDir() (registered by the composition root) instead of sniffing execPath/argv
- synthesize agent_settled from the first terminal agent_end on omp
- read the live model through ctx.models.current() and prefer it over the stored model on omp
- adapt omp Settings (reloadFromDisk, global enabledModels) at the SDK boundary and drop scoped-model persistence on omp
This commit is contained in:
evandrodevbr committed 2026-10-01 10:07:47 -03:00
1 parent 52c9fbeaf3
commit 2aefc7ef5a
12 files changed
+232 -55

No files matched your search

+1
View File
@@ -639,6 +639,7 @@ export function registerTelegramLifecycleRuntimeHooks({
if (!isSessionContextActive(ctx)) return;
activityRuntime.onAgentSettled();
},
settleOnTerminalAgentEnd: Pi.isOmpHost(pi),
onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
isConfigured: configStore.hasBotToken,
isProactivePushEnabled,
+10
View File
@@ -23,6 +23,7 @@ import type {
ToolExecutionStartEvent,
ToolExecutionUpdateEvent,
} from "./pi.ts";
import { isTerminalAgentEnd } from "./pi.ts";
let resetTransportReplyDedupFn: (() => void) | undefined;
@@ -110,6 +111,12 @@ export interface TelegramLifecycleRegistrationDeps {
event: AgentSettledEvent,
ctx: ExtensionContext,
) => Promise<void> | void;
/**
* Hosts that never emit `agent_settled` (omp) get it synthesized from the
* first terminal `agent_end`. Leave unset on hosts that emit it natively so
* the settle point is not announced early.
*/
settleOnTerminalAgentEnd?: boolean;
}
export interface TelegramSessionLifecycleHooks {
@@ -562,6 +569,9 @@ export function registerTelegramLifecycleHooks(
pi.on("agent_end", async (event, ctx) => {
if (!isActive(ctx)) return;
await deps.onAgentEnd(event, ctx);
if (deps.settleOnTerminalAgentEnd && isTerminalAgentEnd(event)) {
await deps.onAgentSettled?.({ type: "agent_settled" }, ctx);
}
});
pi.on("agent_settled", async (event, ctx) => {
if (!isActive(ctx)) return;
+16 -2
View File
@@ -55,15 +55,25 @@ export type CurrentModelRuntime<
> = CurrentModelStore<TContext, TModel> &
CurrentModelUpdateRuntime<TContext, TModel>;
/**
* `preferContextModel` makes the host's live context model win over the stored
* one. Pi pushes model changes through `model_select`, so its stored value is
* current; omp has no such event, so its stored value would go stale after a
* local `/model` switch.
*/
export function createCurrentModelStore<
TContext,
TModel extends MenuModel = MenuModel,
>(
getContextModel: (ctx: TContext) => TModel | undefined,
preferContextModel = false,
): CurrentModelStore<TContext, TModel> {
let currentModel: TModel | undefined;
return {
get: (ctx) => currentModel ?? getContextModel(ctx),
get: (ctx) =>
preferContextModel
? (getContextModel(ctx) ?? currentModel)
: (currentModel ?? getContextModel(ctx)),
getStored: () => currentModel,
set: (model) => {
currentModel = model;
@@ -96,8 +106,12 @@ export function createCurrentModelRuntime<
>(deps: {
getContextModel: (ctx: TContext) => TModel | undefined;
updateStatus: (ctx: TContext) => void;
preferContextModel?: boolean;
}): CurrentModelRuntime<TContext, TModel> {
const store = createCurrentModelStore(deps.getContextModel);
const store = createCurrentModelStore(
deps.getContextModel,
deps.preferContextModel,
);
return {
...store,
...createCurrentModelUpdateRuntime({
+25 -17
View File
@@ -1,19 +1,32 @@
/**
* Telegram bridge path resolution for Pi-compatible runtimes
* Zones: telemetry paths, filesystem, runtime identity
* Owns agent-dir detection and extension-local path derivation
* Owns agent-dir resolution and extension-local path derivation
*
* This domain is pure/path-only: it resolves directories and file paths
* from environment and runtime identity. It does not read config, manage
* state, or import broader Telegram domains.
* This domain is pure/path-only: it resolves directories and file paths from
* the environment and the host runtime's own agent directory. It does not read
* config, manage state, or import broader Telegram domains or the Pi SDK.
*/
import { homedir } from "node:os";
import { join, resolve } from "node:path";
export interface TelegramAgentDirResolutionInput {
env?: Partial<Pick<NodeJS.ProcessEnv, "PI_CODING_AGENT_DIR">>;
execPath?: string;
argv?: readonly string[];
}
let hostAgentDirResolver: (() => string) | undefined;
/**
* Register the host runtime's own agent-directory lookup (the Pi SDK
* `getAgentDir()`). It is registered by the composition root instead of
* imported here: importing the Pi SDK from this leaf made every domain that
* touches a path, and every child process the test suite spawns, pay about a
* second of SDK startup.
*/
export function setHostAgentDirResolver(
resolver: (() => string) | undefined,
): void {
hostAgentDirResolver = resolver;
}
/**
@@ -21,23 +34,18 @@ export interface TelegramAgentDirResolutionInput {
*
* Precedence:
* 1. `PI_CODING_AGENT_DIR` env variable, when explicitly set.
* 2. Detect Pi-compatible runtime identity from the executable or argv[1]
* (e.g. OMP vs standard Pi agent).
* 3. Fallback: `~/.pi/agent`.
* 2. The registered host agent directory. Pi returns `~/.pi/agent`; omp's Pi
* shim returns its profile-aware directory (default `~/.omp/agent`). The
* executable and argv are not inspected: omp installed through Bun runs as
* `bun .../dist/cli.js`, which neither contains nor starts with `omp`.
* 3. Fallback when no host is registered: `~/.pi/agent`.
*/
export function resolveAgentDir(
input: TelegramAgentDirResolutionInput = {},
): string {
const env = input.env ?? process.env;
if (env.PI_CODING_AGENT_DIR) return resolve(env.PI_CODING_AGENT_DIR);
const execPath = input.execPath ?? process.execPath;
const argv = input.argv ?? process.argv;
const execBasename = execPath.toLowerCase().split(/[\\/]/u).pop() ?? "";
const argv1Last = (argv[1] ?? "").toLowerCase().split(/[\\/]/u).pop() ?? "";
if (execBasename.startsWith("omp") || argv1Last.startsWith("omp")) {
return join(homedir(), ".omp", "agent");
}
return join(homedir(), ".pi", "agent");
return hostAgentDirResolver?.() ?? join(homedir(), ".pi", "agent");
}
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
+85 -2
View File
@@ -20,6 +20,7 @@ import {
type SessionStartEvent,
type SlashCommandInfo,
SettingsManager,
getAgentDir,
} from "@earendil-works/pi-coding-agent";
export type {
@@ -39,6 +40,13 @@ export type {
SlashCommandInfo,
};
/**
* Agent data directory owned by the host runtime. Pi returns its own root and
* omp's legacy Pi shim forwards omp's profile-aware directory, so no argv or
* executable sniffing is needed.
*/
export { getAgentDir as getHostAgentDir };
export interface ToolExecutionStartEvent {
type: "tool_execution_start";
toolCallId: string;
@@ -69,6 +77,26 @@ export interface PiSettingsManager {
setEnabledModels: (patterns: string[] | undefined) => void;
}
/**
* omp's `Settings` is what its legacy shim returns from
* `SettingsManager.create()`. It has no `reload()` or `get/setEnabledModels()`:
* it reloads through `reloadFromDisk()` and keeps `enabledModels` as a
* path-scoped typed setting.
*/
interface OmpSettingsSurface {
reloadFromDisk: () => Promise<void>;
flush: () => Promise<void>;
getGlobalSettings: () => { enabledModels?: unknown };
}
function isOmpSettings(value: object): value is OmpSettingsSurface {
return (
"reloadFromDisk" in value &&
"flush" in value &&
"getGlobalSettings" in value
);
}
export type PiSlashCommandInfo = SlashCommandInfo;
export type PiRunMode = "tui" | "rpc" | "json" | "print";
@@ -150,7 +178,22 @@ export function createExtensionApiRuntimePorts(
}
export function createSettingsManager(cwd: string): PiSettingsManager {
return SettingsManager.create(cwd);
const manager: PiSettingsManager = SettingsManager.create(cwd);
if (!isOmpSettings(manager)) return manager;
return {
reload: () => manager.reloadFromDisk(),
flush: () => manager.flush(),
getEnabledModels: () => {
const enabled = manager.getGlobalSettings().enabledModels;
return Array.isArray(enabled) &&
enabled.every((pattern) => typeof pattern === "string")
? enabled
: undefined;
},
setEnabledModels: () => {
throw new Error("Scoped model persistence is not supported on omp.");
},
};
}
export function createScopedModelPatternPersister(deps: {
@@ -167,16 +210,56 @@ export function createScopedModelPatternPersister(deps: {
};
}
interface OmpModelQuery {
current: () => ExtensionContext["model"];
}
function hasOmpModelQuery(
ctx: ExtensionContext,
): ctx is ExtensionContext & { models: OmpModelQuery } {
return (
"models" in ctx &&
typeof ctx.models === "object" &&
ctx.models !== null &&
"current" in ctx.models &&
typeof ctx.models.current === "function"
);
}
/**
* omp builds command contexts by spreading a base context, which evaluates the
* `model` getter once: a context stored from `/telegram-connect` would report
* the model of that moment forever. omp's `ctx.models.current()` closes over
* the live session model, so prefer it when present.
*/
export function getExtensionContextModel(
ctx: ExtensionContext,
): ExtensionContext["model"] {
return ctx.model;
return hasOmpModelQuery(ctx) ? ctx.models.current() : ctx.model;
}
export function getExtensionContextCwd(ctx: ExtensionContext): string {
return ctx.cwd;
}
/**
* True when the extension API belongs to omp. omp's `ExtensionAPI` carries
* `typebox` and `pi` module facades that upstream Pi's API does not have.
*/
export function isOmpHost(api: ExtensionAPI): boolean {
return "typebox" in api && "pi" in api;
}
/**
* omp marks an `agent_end` with `willContinue: true` whenever it already
* scheduled a retry, compaction continuation, todo/plan reminder, or async
* wake. Any other `agent_end` is terminal, which is what Pi reports through
* `agent_settled`.
*/
export function isTerminalAgentEnd(event: AgentEndEvent): boolean {
return !("willContinue" in event && event.willContinue === true);
}
export function isExtensionContextIdle(ctx: ExtensionContext): boolean {
return ctx.isIdle();
}