Skip to content

feat: one Headroom process per Pi process - #1

Open
nickadminroot wants to merge 2 commits into
mainfrom
feat/per-pi-headroom-session
Open

feat: one Headroom process per Pi process#1
nickadminroot wants to merge 2 commits into
mainfrom
feat/per-pi-headroom-session

Conversation

@nickadminroot

Copy link
Copy Markdown
Owner

Summary

Replace the global Headroom supervisor pattern with a per-Pi session runtime. Each Pi process now automatically spawns exactly one child Headroom process that:

  • starts from the current working directory
  • builds a Code Graph for that directory
  • serves the current Pi session
  • supports openai-codex and opencode-go
  • shuts down when Pi shuts down

Architecture

Pi process → Headroom child process
  ├── Code Graph (cwd-specific)
  ├── openai-codex (/backend-api)
  └── opencode-go (anthropic-messages or openai-completions)

Changes

  • Dynamic port allocation on 127.0.0.1
  • Health polling with retry logic
  • Windows process tree termination via taskkill /T
  • Orphan cleanup on startup
  • Singleton runtime via globalThis (survives /reload)
  • Single-flight start prevents race conditions
  • Restart limiter (max 3 per 10 minutes)

New Commands

  • /headroom-status — show process status
  • /headroom-restart — restart Headroom
  • /headroom-stop — stop Headroom
  • /headroom-log — show log path

Testing

All 47 tests pass. TypeScript compiles without errors.

Replace global supervisor pattern with one Headroom process per Pi process.

Architecture:
- Each Pi spawns exactly one Headroom child process
- Headroom starts from current working directory with Code Graph
- Supports openai-codex, opencode-go, and all existing providers
- Dynamic port allocation on 127.0.0.1
- Health polling with retry logic
- Windows process tree termination via taskkill /T
- Orphan cleanup on startup
- Singleton runtime via globalThis survives /reload
- Single-flight start prevents race conditions
- Restart limiter (max 3 per 10 minutes)

New modules:
- types.ts: core types and interfaces
- session-runtime.ts: state machine and lifecycle
- headroom-process.ts: spawn and health check
- windows-process.ts: taskkill-based termination
- port-allocation.ts: dynamic port allocation
- proxy-options.ts: CLI args and environment
- request-identity.ts: per-request headers
- headroom-retrieve.ts: CCR retrieval tool
- headroom-commands.ts: /headroom-status, restart, stop, log
- runtime-state.ts: globalThis singleton
- provider-registry.ts: updated with openai-codex and opencode-go

Tests:
- Provider routing (Codex, OpenCode Go, existing providers)
- Port allocation
- Session runtime lifecycle
- Request identity headers
- CCR retrieval with fake server
- Windows path handling
- Process lifecycle
Fixes from code review:

1. opencode-go routing: use computed baseUrl in registerProvider()
   instead of always registering root URL.

2. Limit managed providers to openai-codex and opencode-go only.
   Old providers (openai, anthropic, etc.) are not routed through
   Headroom in this per-Pi mode.

3. Startup error kills child process: when health check fails,
   terminate the Headroom process tree before setting state to failed.

4. Write metadata immediately after spawn (before health check)
   so orphan scanner can find processes that failed to start.

5. Wire restart limiter: check canAutomaticallyRestart() in
   startInternal() and record successful starts.

6. model_select uses event.model instead of ctx.model for routing.

7. /headroom-status shows runtime.ownerPid as Pi PID and
   info.pid as Headroom PID (was showing same PID twice).

8. opencode-go throws error for unknown model.api instead of
   silently falling back to root URL.

9. headroom_retrieve uses extensionCtx.cwd instead of
   hardcoded process.cwd().

10. Session ID uses monotonic counter to differentiate
    session switches within same Pi process.

11. Type definitions narrowed to match actual managed providers.
@nickadminroot

nickadminroot commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Обновлённое review PR #1

Вердикт

Статус остаётся REQUEST_CHANGES.

Оценка текущей реализации: 6/10.

Основная архитектура «один Headroom-процесс на один процесс Pi» остаётся правильной. Однако требования к Code Graph изменились: по умолчанию Headroom не должен владеть графом, индексом или filesystem watcher.

В рекомендуемой конфигурации:

Pi
├── codebase-memory-mcp
│   ├── MCP graph tools
│   ├── repository indexing
│   └── filesystem watcher
│
└── Headroom
    ├── provider routing
    ├── context compression
    ├── code-aware processing
    ├── tool-result compression
    └── CCR retrieval

Headroom не должен запускать второй watcher и повторно инициировать индексацию того же worktree.

Что уже сделано правильно

PR уже содержит полезную основу:

  • один runtime на процесс Pi;
  • один дочерний Headroom;
  • динамический localhost port;
  • Windows-compatible запуск;
  • ранний warmup;
  • health readiness barrier;
  • завершение дерева процессов;
  • orphan cleanup;
  • корректную маршрутизацию openai-codex;
  • model-dependent маршрутизацию opencode-go;
  • раннюю запись PID metadata;
  • cleanup после health timeout;
  • ограничение managed providers до openai-codex и opencode-go.

Эти части следует сохранить.

Новый архитектурный блокер: безусловный --code-graph

Сейчас Headroom запускается с:

--code-aware
--code-graph
--memory
--memory-storage project

Безусловный --code-graph больше не соответствует целевой архитектуре.

На машине уже работает codebase-memory-mcp, подключённый к Pi через MCP. Он отвечает за:

  • индексирование репозитория;
  • автоматическое обновление индекса;
  • graph search;
  • architecture queries;
  • call graph;
  • impact analysis;
  • работу с несколькими repository paths и worktree.

При сохранении --code-graph получится два независимых watcher:

codebase-memory-mcp watcher
+
Headroom CodeGraphWatcher

Оба будут реагировать на изменения файлов и инициировать операции над индексом. На Windows это создаёт лишнюю нагрузку и риск параллельных операций с одним постоянным хранилищем.

По умолчанию владельцем графа должен быть codebase-memory-mcp.

Требуемый Code Graph mode

Добавить настройку:

export type CodeGraphMode =
  | "external"
  | "headroom"
  | "off";

Переменная окружения:

PI_HEADROOM_CODE_GRAPH_MODE

Значение по умолчанию:

external

Поведение:

external

не передавать --code-graph
передавать --code-aware
не запускать Headroom watcher
считать владельцем индекса codebase-memory-mcp

headroom

передавать --code-graph
Headroom запускает собственный watcher
режим opt-in, не default

Этот режим нужен для пользователей, у которых нет внешнего индексатора.

off

не передавать --code-graph
не заявлять наличие внешнего Code Graph

При этом --code-aware можно оставить, если он не требует graph watcher.

Некорректное значение environment variable должно приводить к понятной startup error, а не к silent fallback.

Аргументы Headroom по умолчанию

Для external использовать:

proxy
--host 127.0.0.1
--port <dynamic-port>
--mode token
--code-aware
--memory
--memory-storage project

Не добавлять:

--code-graph

--intercept-tool-results

Флаг можно поддержать как отдельную экспериментальную настройку, но не включать по умолчанию.

Например:

PI_HEADROOM_INTERCEPT_TOOL_RESULTS=1

Только при этом значении добавлять:

--intercept-tool-results

До включения по умолчанию обязательно проверить:

  • результат встроенного Pi read;
  • результат grep;
  • результат find;
  • результат MCP search_graph;
  • результат MCP search_code;
  • большой JSON tool result;
  • наличие корректного CCR marker;
  • успешное раскрытие через headroom_retrieve.

Если версия Headroom не поддерживает этот флаг, startup должен выдавать понятную ошибку или функция должна быть отключена.

Исключение точных graph tools из сжатия

Добавить необязательную настройку:

PI_HEADROOM_EXCLUDE_TOOLS

Формат:

tool_a,tool_b,tool_c

Парсер должен:

  • разделять по запятой;
  • обрезать пробелы;
  • удалять пустые элементы;
  • удалять дубликаты;
  • не подставлять имена tools автоматически.

После проверки реальных wire names пользователь сможет установить, например:

PI_HEADROOM_EXCLUDE_TOOLS=mcp__codebase_memory__query_graph,mcp__codebase_memory__analyze_impact

Имена в документации должны быть представлены только как примеры.

Нельзя предполагать, что wire name равен UI name. Его нужно получить из реального request log Headroom.

На первом этапе рекомендуется исключать только инструменты, где точная структура результата критична:

  • точный graph query;
  • impact analysis;
  • call path;
  • callers/callees, если порядок и структура являются значимыми.

Большие обзорные результаты можно оставлять доступными для сжатия:

  • architecture;
  • semantic query;
  • search code;
  • search graph.

/headroom-status

Статус должен отражать фактический режим.

Для external:

Code Graph mode: external
Code Graph owner: codebase-memory-mcp
Headroom Code Graph watcher: disabled
Code-aware compression: enabled

Для headroom:

Code Graph mode: headroom
Code Graph owner: Headroom
Headroom Code Graph watcher: enabled

Для off:

Code Graph mode: off
Code Graph owner: none
Headroom Code Graph watcher: disabled

Нельзя всегда показывать:

Code Graph: enabled

Worktree

Каждый worktree должен считаться отдельным repository path:

C:\src\repo
C:\src\repo-worktrees\feature-a
C:\src\repo-worktrees\feature-b

Headroom продолжает запускаться из ctx.cwd, но в режиме external он не индексирует эту папку.

Индексированием занимается MCP.

Документация должна явно требовать:

codebase-memory-mcp config set auto_index true

При первой MCP-сессии в worktree нужно убедиться, что MCP индексирует фактический путь worktree, а не основной checkout.

Не надо вызывать index_repository из pi-headroom. Это зона ответственности MCP integration.

headroom_retrieve всё ещё является блокером

Tool по-прежнему написан не по настоящему Pi extension API.

Нужно:

import type {
  ExtensionAPI,
  ExtensionContext,
} from "@earendil-works/pi-coding-agent";

import { Type } from "typebox";

Регистрация должна иметь вид:

pi.registerTool({
  name: "headroom_retrieve",
  label: "Headroom Retrieve",
  description: "...",
  parameters: Type.Object({
    hash: Type.String(),
    query: Type.Optional(Type.String()),
  }),
  async execute(
    toolCallId,
    params,
    signal,
    onUpdate,
    ctx,
  ) {
    // ...
  },
});

Возвращаемое значение:

{
  content: [
    {
      type: "text",
      text: retrievedText,
    },
  ],
  details: {
    hash: params.hash,
  },
}

Использовать:

ctx.cwd
signal

Нельзя использовать собственный двухаргументный execute(args, ctx).

Тесты должны вызывать реальную пятиаргументную сигнатуру.

Не запускать Headroom для unmanaged provider

Extension управляет только:

openai-codex
opencode-go

Поэтому before_agent_start не должен блокировать обычные anthropic, openai, google и другие provider.

До runtime.ensureReady() выполнить:

if (!isManagedModel(ctx.model)) {
  return;
}

Ранний warmup на session_start можно выполнять только если текущая модель managed.

Если модели ещё нет, дождаться model_select или первого before_agent_start.

Restart limiter

Счётчик должен учитывать только аварийные автоматические рестарты.

Не считать:

  • первоначальный запуск;
  • ручной /headroom-restart;
  • ожидаемый restart после смены cwd;
  • restart после /new;
  • restart после /resume;
  • restart после /fork.

Сейчас успешные обычные старты записываются в limiter, из-за чего нормальные lifecycle operations могут заблокировать Headroom.

Записывать restart timestamp только после неожиданного crash и последующей автоматической попытки восстановления.

Session identity

Не использовать module-local counter.

При /new, /resume и /fork extension перезагружается, поэтому counter снова начинается с нуля.

Использовать:

ctx.sessionManager.getSessionFile()

Рекомендуемый идентификатор:

sha256(
  runtimeId +
  "\0" +
  sessionFileOrEphemeralId
)

Результат должен быть ASCII.

Windows headers

Не помещать сырой кириллический Windows path в HTTP header.

Путь вида:

C:\Users\Владимир\repo

может быть отклонён Node HTTP implementation как невалидный ByteString.

Рекомендуемая схема:

x-headroom-cwd-hash = sha256(canonicalCwd)
x-headroom-session-id = sha256(runtimeId + sessionFile)
x-headroom-user-id = ASCII-safe value

Если Headroom требует именно x-headroom-cwd, нужно:

  • проверить допустимый формат;
  • использовать ASCII-safe encoding;
  • либо передавать отдельный stable project ID.

Не использовать полный cwd внутри session ID.

PI_HEADROOM_FAIL_OPEN

Переменная документирована, но не реализована.

Нужно выбрать одно:

  1. Реализовать fail-open.
  2. Удалить переменную из README и типов.

Для managed provider при fail-open:

Headroom startup failed
→ показать warning
→ не регистрировать proxy override
→ позволить Pi использовать штатный provider URL

Нельзя выполнять fallback молча.

README

Исправить документацию.

Сейчас она не должна заявлять, что все старые провайдеры работают через Headroom.

Нужно написать:

Managed providers:
- openai-codex
- opencode-go

Other providers:
- continue to work directly through Pi
- are not blocked by Headroom readiness

Добавить раздел про внешний MCP:

Recommended Code Graph setup:
- codebase-memory-mcp connected to Pi through MCP
- auto_index enabled
- PI_HEADROOM_CODE_GRAPH_MODE=external

Добавить предупреждение:

Do not enable Headroom --code-graph while the same repository
is already watched by codebase-memory-mcp, unless duplicate
watchers are intentionally desired.

CI

Для head commit всё ещё нет подтверждённого GitHub Actions run.

Добавить Windows CI:

runs-on: windows-latest

Шаги:

npm ci
npm run typecheck
npm test

Typecheck должен использовать реальные типы Pi, а не локальные упрощённые интерфейсы.

Итоговый статус

До merge обязательны:

  1. external | headroom | off.
  2. Default external.
  3. Удаление безусловного --code-graph.
  4. Реальный Pi API для headroom_retrieve.
  5. Пропуск readiness для unmanaged providers.
  6. Исправление restart limiter.
  7. Stable session ID.
  8. ASCII-safe headers.
  9. Исправление README.
  10. Windows CI.

После этого PR можно переводить к реальным integration tests.

@nickadminroot

Copy link
Copy Markdown
Owner Author

Дальнейшие инструкции агенту

Внеси следующий fix-коммит в текущую ветку PR.

1. Добавить Code Graph mode

В proxy-options.ts добавить:

export type CodeGraphMode =
  | "external"
  | "headroom"
  | "off";

Добавить:

export function resolveCodeGraphMode(
  env: NodeJS.ProcessEnv = process.env,
): CodeGraphMode {
  const raw =
    env.PI_HEADROOM_CODE_GRAPH_MODE
      ?.trim()
      .toLowerCase() ?? "external";

  switch (raw) {
    case "external":
    case "headroom":
    case "off":
      return raw;

    default:
      throw new Error(
        `Invalid PI_HEADROOM_CODE_GRAPH_MODE: ${raw}. ` +
        `Expected external, headroom, or off.`,
      );
  }
}

2. Переделать buildHeadroomArgs

Новая сигнатура:

export interface BuildHeadroomArgsOptions {
  codeGraphMode: CodeGraphMode;
  interceptToolResults: boolean;
  excludeTools: string[];
}

export function buildHeadroomArgs(
  port: number,
  options: BuildHeadroomArgsOptions,
): string[];

Базовые аргументы:

const args = [
  "proxy",
  "--host",
  "127.0.0.1",
  "--port",
  String(port),
  "--mode",
  "token",
  "--code-aware",
  "--memory",
  "--memory-storage",
  "project",
];

Только для:

options.codeGraphMode === "headroom"

добавить:

args.push("--code-graph");

Для external и off не добавлять.

3. Добавить экспериментальный intercept tool results

Разрешить:

PI_HEADROOM_INTERCEPT_TOOL_RESULTS=1

Parser:

export function resolveInterceptToolResults(
  env: NodeJS.ProcessEnv = process.env,
): boolean {
  return env.PI_HEADROOM_INTERCEPT_TOOL_RESULTS === "1";
}

Если true:

args.push("--intercept-tool-results");

Default false.

4. Добавить exclude tools

Переменная:

PI_HEADROOM_EXCLUDE_TOOLS

Parser:

export function resolveExcludeTools(
  env: NodeJS.ProcessEnv = process.env,
): string[] {
  return [
    ...new Set(
      (env.PI_HEADROOM_EXCLUDE_TOOLS ?? "")
        .split(",")
        .map((name) => name.trim())
        .filter(Boolean),
    ),
  ];
}

Передавай flag только при непустом массиве.

Сначала проверь точную CLI-сигнатуру Headroom.

Если формат:

--exclude-tools tool_a,tool_b

то передавай один comma-separated argument.

Не придумывай формат без проверки установленной версии Headroom.

5. Передать options в runtime

В HeadroomSessionRuntime сохранить:

private readonly codeGraphMode: CodeGraphMode;
private readonly interceptToolResults: boolean;
private readonly excludeTools: string[];

Инициализировать из environment в constructor.

В spawnAndWait():

const args = buildHeadroomArgs(port, {
  codeGraphMode: this.codeGraphMode,
  interceptToolResults: this.interceptToolResults,
  excludeTools: this.excludeTools,
});

Добавить getters для status command:

getCodeGraphMode(): CodeGraphMode;
isToolResultInterceptionEnabled(): boolean;
getExcludedTools(): readonly string[];

6. Обновить /headroom-status

Для external выводить:

Code Graph mode: external
Code Graph owner: codebase-memory-mcp
Headroom watcher: disabled

Для headroom:

Code Graph mode: headroom
Code Graph owner: Headroom
Headroom watcher: enabled

Для off:

Code Graph mode: off
Code Graph owner: none
Headroom watcher: disabled

Также вывести:

Tool result interception: enabled|disabled
Excluded tools: <list>|none

7. Переписать headroom_retrieve на реальный Pi API

Удалить локальные mock definitions для tool registration.

Использовать реальные импорты:

import type {
  ExtensionAPI,
  ExtensionContext,
} from "@earendil-works/pi-coding-agent";

import { Type } from "typebox";

Регистрация:

pi.registerTool({
  name: "headroom_retrieve",
  label: "Headroom Retrieve",
  description:
    "Retrieve original content referenced by a Headroom CCR hash.",
  parameters: Type.Object({
    hash: Type.String({
      description: "Hash from a Headroom CCR marker",
    }),
    query: Type.Optional(
      Type.String({
        description:
          "Optional query for selecting a relevant part",
      }),
    ),
  }),

  async execute(
    _toolCallId,
    params,
    signal,
    _onUpdate,
    ctx,
  ) {
    // implementation
  },
});

Объединить timeout signal и переданный Pi signal.

Возвращать настоящий tool result:

return {
  content: [
    {
      type: "text",
      text,
    },
  ],
  details: {
    hash: params.hash,
  },
};

Обновить тесты под пятиаргументный execute.

8. Использовать реальные Pi types

В index.ts:

import type {
  ExtensionAPI,
  ExtensionContext,
} from "@earendil-works/pi-coding-agent";

Не использовать локальный ExtensionAPI.

В commands использовать настоящий ExtensionCommandContext, если требуется.

Удалить из types.ts собственные определения:

ExtensionAPI
ExtensionCtx
ExtensionUI

Оставить только внутренние типы extension.

Добавить runtime dependency:

"typebox": "<совместимая версия>"

или корректный peer dependency, если package policy проекта гарантирует наличие TypeBox.

Учитывай, что production install не устанавливает devDependencies.

9. Не блокировать unmanaged providers

В session_start:

if (!isManagedModel(ctx.model)) {
  updateUiStatus(ctx, runtime);
  return;
}

В before_agent_start:

if (!isManagedModel(ctx.model)) {
  return;
}

В model_select:

  • если новая модель managed — запустить warmup или применить route;
  • если unmanaged — не вызывать ensureReady;
  • не останавливать Headroom автоматически, он может оставаться прогретым.

10. Исправить restart limiter

Удалить:

this.recordAutomaticRestart();

из обычного успешного startInternal().

Добавить отдельный path:

ensureReady(cwd, {
  automaticRecovery: true,
});

или внутренний flag, который устанавливается только после unexpected child exit.

Timestamp записывать только перед автоматической recovery attempt.

Не учитывать manual restart и session lifecycle.

Добавить тест:

initial start
/new
/resume
/fork

не должны исчерпывать limiter.

Отдельный тест:

unexpected crash × 3

должен блокировать четвёртую automatic recovery.

11. Исправить session ID

В session_start использовать:

const sessionFile =
  ctx.sessionManager.getSessionFile();

Для ephemeral session создать стабильный ID внутри runtime:

runtime.getOrCreateEphemeralSessionId();

Session ID:

sha256(
  `${runtime.runtimeId}\0${sessionFile ?? ephemeralId}`,
).slice(0, 32);

Не использовать cwd внутри session ID.

12. Сделать headers ASCII-safe

Не помещать сырой cwd в session ID.

Перед отправкой проверить каждый header на ASCII.

Минимально:

function asciiHeaderHash(value: string): string {
  return createHash("sha256")
    .update(value, "utf8")
    .digest("hex");
}

Рекомендуемые значения:

x-headroom-session-id = hash
x-headroom-user-id = sanitized ASCII username or hash

Для project/worktree identity:

x-headroom-project-id = hash(canonical cwd)

Перед изменением x-headroom-cwd проверить, требует ли Headroom сам путь. Если требует, использовать поддерживаемый ASCII encoding и добавить test с:

C:\Users\Владимир\repo

13. Реализовать или удалить fail-open

Предпочтительно реализовать.

В before_agent_start:

try {
  await runtime.ensureReady(ctx.cwd);
} catch (error) {
  if (!runtime.isFailOpen()) {
    throw error;
  }

  ctx.ui.notify(
    "Headroom failed to start. Continuing with direct provider connection.",
    "warning",
  );

  return;
}

Не регистрировать proxy override при failure.

Если реализация откладывается, удалить:

PI_HEADROOM_FAIL_OPEN

из README и внутренних options.

14. Исправить UI levels

Настоящий Pi API принимает:

info
warning
error

Заменить:

warn → warning
success → info

15. Обновить README

Задокументировать:

codebase-memory-mcp config set auto_index true

Default:

PI_HEADROOM_CODE_GRAPH_MODE=external

Добавить примеры:

$env:PI_HEADROOM_CODE_GRAPH_MODE = "external"
$env:PI_HEADROOM_INTERCEPT_TOOL_RESULTS = "1"
$env:PI_HEADROOM_EXCLUDE_TOOLS = "mcp__codebase_memory__query_graph,mcp__codebase_memory__analyze_impact"

Явно указать, что реальные tool names нужно брать из request logs.

Удалить утверждение, что все остальные providers маршрутизируются через Headroom.

16. Добавить тесты

Минимум:

external does not add --code-graph
headroom adds --code-graph
off does not add --code-graph
invalid mode throws
intercept disabled by default
intercept flag enabled via env
exclude tools parser trims and deduplicates
status reports external owner
unmanaged provider does not start Headroom
real five-argument tool execute
Pi abort signal cancels retrieve
Cyrillic cwd does not create invalid headers
normal session switches do not consume restart limit
automatic crashes do consume restart limit

17. Добавить Windows CI

Создать:

.github/workflows/ci.yml

С jobs на:

windows-latest

Команды:

npm ci
npm run typecheck
npm test

После push проверить, что workflow run появился и завершился успешно.

18. Рекомендуемый commit

fix: use external MCP as default code graph owner

Commit должен включать:

  • CodeGraphMode;
  • удаление default --code-graph;
  • status changes;
  • real Pi tool API;
  • managed-provider guard;
  • limiter fix;
  • session ID fix;
  • ASCII-safe headers;
  • README;
  • tests;
  • CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant