Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/en/docs/how-to/configure-codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ an `authentication_failed` diagnostic; MCP tools remain unavailable without bloc
If the Server is unavailable, hook recall and capture fail open. Codex work continues, and explicit Memory tools
report that the service is unavailable.

For a normal empty result or recall failure, the Hook writes a content-free JSON diagnostic to stderr. Outcomes include
`empty`, `authentication_failed`, `version_mismatch`, `server_unavailable`, and `invalid_response`. The event never
contains the query, scope, prepared content, citation, response body, or authorization value.
For a normal empty result or recall failure, the Hook emits a content-free JSON diagnostic. Failure outcomes are
returned through the top-level `systemMessage` in the successful stdout hook response; `empty` remains a local
diagnostic. Outcomes include `empty`, `authentication_failed`, `version_mismatch`, `server_unavailable`, and
`invalid_response`. The event never contains the query, scope, prepared content, citation, response body, or
authorization value.
46 changes: 39 additions & 7 deletions docs/en/docs/how-to/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,45 @@ powercontext capabilities

`Memory extraction: disabled` means the Server has no generation model.

## Host-visible integration diagnostics

The Codex, Claude Code, DSH, OpenClaw, Pi, and Hermes integrations are fail-open: a PowerContext outage does not
block the host task. They also expose a bounded, content-free diagnostic through the host's supported channel:

| Host | Diagnostic channel | Component |
| --- | --- | --- |
| Codex | Hook stdout `systemMessage` | `powercontext.codex.recall` |
| Claude Code | Hook stdout `systemMessage` | `powercontext.claude_code.recall` |
| DSH | Host logger warning | `powercontext.dsh` |
| OpenClaw | Plugin logger warning | `powercontext.openclaw` |
| Pi | Host terminal warning | `powercontext.pi` |
| Hermes | Python host logger warning | `powercontext.hermes` |

For example, a transport failure is returned in the hook's top-level `systemMessage`; its value is a single-line,
content-free JSON event such as:

```json
{"systemMessage":"{\"component\":\"powercontext.codex.recall\",\"event\":\"context_prepare\",\"outcome\":\"server_unavailable\",\"recovery\":\"powercontext doctor\"}"}
```

The stable outcomes remain distinct: `authentication_failed`, `version_mismatch`, `server_unavailable`, and
`invalid_response`. Diagnostics never include prompts, recalled content, scopes, URLs, credentials, response bodies,
or exception text. Repeated outcomes are deduplicated within one invocation and throttled for 60 seconds using local
state shared across hook processes; a diagnostic failure never changes the host task result.

Bub is not included in this first host-diagnostic slice. Its integration will be qualified separately when its host
diagnostic channel and native lifecycle behavior are specified.

## The coding agent continues when the Server is down

This is expected. The Codex, Claude Code, and Pi integrations fail open so a Memory outage cannot block ordinary work.
Restart the Server to restore recall and capture; the existing database is reopened automatically.
This is expected. The supported integrations fail open so a Memory outage cannot block ordinary work. Inspect the
host-visible diagnostic and run `powercontext doctor`; restart the Server to restore recall and capture. The existing
database is reopened automatically.

## Codex does not inject recalled context

Inspect the Hook's single-line JSON event on stderr. `empty` means the Runtime prepared no context for this turn.
For failures, inspect the Hook's top-level `systemMessage`; its value is the single-line JSON event. `empty` means the
Runtime prepared no context for this turn and remains a local diagnostic rather than a host warning.
`version_mismatch` means the installed plugin expects
`POST /v1/context/prepare` but the Server does not provide it—reinstall the plugin and tool from the same ref, then
restart the Server. `server_unavailable` and `invalid_response` distinguish transport and contract failures. These
Expand All @@ -204,7 +235,8 @@ powercontext doctor
```

The first command checks the Claude CLI and enabled plugin without contacting the Server. The second checks Server
liveness and readiness. Then inspect the Hook's single-line stderr event. Claude Code uses the same Prepared Context
liveness and readiness. For failures, inspect the Hook's top-level `systemMessage`; its value is the single-line JSON
event. Claude Code uses the same Prepared Context
contract as Codex, with component `powercontext.claude_code.recall`:

| Outcome | Action |
Expand Down Expand Up @@ -241,7 +273,7 @@ powercontext doctor
```

Restart Pi after installing the package or changing `POWERCONTEXT_PI_*` variables. In a new Pi session, run
`/pc doctor` to check the configured Server directly. Recall is intentionally silent and fail-open: if the Server is
unavailable, redirects, times out, or returns an invalid PreparedContext, Pi continues without adding context. Restore
the Server, then run `powercontext capabilities` and confirm that Context versions lists
`/pc doctor` to check the configured Server directly. Recall is fail-open and reports a content-free host terminal
warning when the Server is unavailable, redirects, times out, or returns an invalid PreparedContext; Pi continues
without adding context. Restore the Server, then run `powercontext capabilities` and confirm that Context versions lists
`powercontext.prepared-context.v1`.
7 changes: 4 additions & 3 deletions docs/zh/docs/how-to/configure-codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Codex 会话。

Server 不可用时,Hook 的恢复和采集会正常降级,不会阻塞 Codex。显式 Memory 工具会报告服务不可用。

正常空结果或召回失败时,Hook 会向 stderr 写一行不含正文的 JSON 诊断。outcome 包括 `empty`、
`authentication_failed`、`version_mismatch`、`server_unavailable` 和 `invalid_response`;事件不会包含 query、
scope、prepared content、citation、response body 或 authorization value。
正常空结果或召回失败时,Hook 会输出不含正文的 JSON 诊断。故障 outcome 通过成功 stdout Hook 响应顶层的
`systemMessage` 返回;`empty` 仍只作为本地诊断。outcome 包括 `empty`、`authentication_failed`、
`version_mismatch`、`server_unavailable` 和 `invalid_response`;事件不会包含 query、scope、prepared content、
`citation`、response body 或 authorization value。
39 changes: 33 additions & 6 deletions docs/zh/docs/how-to/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,41 @@ powercontext capabilities

`Memory extraction: disabled` 表示 Server 没有 generation model。

## 宿主可见的集成诊断

Codex、Claude Code、DSH、OpenClaw、Pi 和 Hermes 集成都遵循 fail-open:PowerContext 故障不会阻塞宿主任务。
同时,它们会通过宿主支持的通道输出有界、无内容的诊断:

| 宿主 | 诊断通道 | component |
| --- | --- | --- |
| Codex | Hook stdout `systemMessage` | `powercontext.codex.recall` |
| Claude Code | Hook stdout `systemMessage` | `powercontext.claude_code.recall` |
| DSH | 宿主 logger warning | `powercontext.dsh` |
| OpenClaw | 插件 logger warning | `powercontext.openclaw` |
| Pi | 宿主终端 warning | `powercontext.pi` |
| Hermes | Python 宿主 logger warning | `powercontext.hermes` |

例如,传输失败会通过 Hook 顶层的 `systemMessage` 返回;它的值是类似下面的单行、无内容 JSON 事件:

```json
{"systemMessage":"{\"component\":\"powercontext.codex.recall\",\"event\":\"context_prepare\",\"outcome\":\"server_unavailable\",\"recovery\":\"powercontext doctor\"}"}
```

稳定的 outcome 仍然彼此区分:`authentication_failed`、`version_mismatch`、`server_unavailable` 和
`invalid_response`。诊断不会包含 prompt、召回内容、scope、URL、凭据、响应正文或异常文本。同一次调用内的
相同 outcome 会去重,跨 Hook 进程会使用本地状态限流 60 秒;诊断失败不会改变宿主任务结果。

Bub 不包含在本次第一阶段的宿主诊断切片中。待其宿主诊断通道和原生生命周期行为单独明确并完成支持验证后再纳入。

## Server 停止后编程 Agent 仍继续工作

这是预期行为。Codex、Claude Code 和 Pi 集成都遵循 fail open,Memory 故障不能阻塞普通工作。
重启 Server 后即可恢复召回和采集,现有数据库会被自动重新打开。
这是预期行为。已支持的集成都遵循 fail-open,Memory 故障不能阻塞普通工作。请查看宿主可见的诊断并运行
`powercontext doctor`;重启 Server 后即可恢复召回和采集,现有数据库会被自动重新打开。

## Codex 没有注入召回上下文

查看 Hook 在 stderr 输出的单行 JSON 事件。`empty` 表示 Runtime 没有为本轮准备上下文。`version_mismatch`
对于故障,查看 Hook 顶层 `systemMessage` 中的单行 JSON 事件。`empty` 表示 Runtime 没有为本轮准备上下文,
它仍是本地诊断,不作为宿主 warning。`version_mismatch`
表示已安装插件要求 `POST /v1/context/prepare`,但 Server 尚未提供该接口;请从同一个 ref 重新安装插件和工具
并重启 Server。`server_unavailable` 和 `invalid_response` 分别表示传输与 contract 问题。诊断事件会刻意
省略 query 与准备好的上下文正文。
Expand All @@ -196,7 +223,7 @@ powercontext doctor
```

第一个命令只检查 Claude CLI 和已启用插件,不连接 Server;第二个命令检查 Server liveness 和 readiness。
然后查看 Hook 在 stderr 输出的单行事件。Claude Code 使用与 Codex 相同的 Prepared Context contract,
然后查看 Hook 顶层 `systemMessage` 中的单行事件。Claude Code 使用与 Codex 相同的 Prepared Context contract,
component 为 `powercontext.claude_code.recall`:

| Outcome | 处理方式 |
Expand Down Expand Up @@ -232,6 +259,6 @@ powercontext doctor
```

安装 package 或修改 `POWERCONTEXT_PI_*` 变量后,请重启 Pi。在新的 Pi 会话中运行 `/pc doctor`,直接检查已配置的
Server。召回会刻意静默并正常降级:Server 不可用、重定向、超时或返回无效 PreparedContext 时,Pi 会继续运行且不
添加上下文。恢复 Server 后,运行 `powercontext capabilities`,确认 Context versions 中包含
Server。召回会正常降级,并在 Server 不可用、重定向、超时或返回无效 PreparedContext 时通过宿主终端输出无内容
warning;Pi 会继续运行且不添加上下文。恢复 Server 后,运行 `powercontext capabilities`,确认 Context versions 中包含
`powercontext.prepared-context.v1`。
103 changes: 103 additions & 0 deletions integrations/claude-code/plugins/powercontext/hooks/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright (c) 2026 OceanBase.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Best-effort cross-process throttling for host-visible hook diagnostics."""

from __future__ import annotations

import json
import os
import time
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path

_FAILURE_OUTCOMES = frozenset({"authentication_failed", "version_mismatch", "server_unavailable", "invalid_response"})
_COOLDOWN_SECONDS = 60.0


def _state_path() -> Path:
configured = os.environ.get("POWERCONTEXT_DIAGNOSTIC_STATE_FILE")
if configured and configured.strip():
return Path(configured)
if os.name == "nt":
root = Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData" / "Local")
else:
root = Path(os.environ.get("XDG_STATE_HOME") or Path.home() / ".local" / "state")
return root / "powercontext" / "claude-code-diagnostics.json"


@contextmanager
def _locked(lock_path: Path) -> Iterator[None]:
lock_path.parent.mkdir(parents=True, exist_ok=True)
with lock_path.open("a+b") as lock_file:
if os.name == "nt":
import msvcrt

lock_file.seek(0, os.SEEK_END)
if lock_file.tell() == 0:
lock_file.write(b"\0")
lock_file.flush()
lock_file.seek(0)
msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
else:
import fcntl

fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
try:
yield
finally:
if os.name == "nt":
import msvcrt

lock_file.seek(0)
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
else:
import fcntl

fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)


def should_emit(outcome: str) -> bool:
"""Return whether a diagnostic should be shown to the host user."""

if outcome not in _FAILURE_OUTCOMES:
return True

try:
now = time.time()
state_path = _state_path()
with _locked(state_path.with_name(f"{state_path.name}.lock")):
try:
state = json.loads(state_path.read_text(encoding="utf-8"))
except (FileNotFoundError, OSError, UnicodeDecodeError, json.JSONDecodeError):
state = {}
if not isinstance(state, dict):
state = {}
previous = state.get(outcome)
if (
isinstance(previous, (int, float))
and not isinstance(previous, bool)
and 0 <= now - previous < _COOLDOWN_SECONDS
):
return False

state[outcome] = now
temporary_path = state_path.with_name(f".{state_path.name}.{os.getpid()}.tmp")
temporary_path.write_text(json.dumps(state, separators=(",", ":")), encoding="utf-8")
os.replace(temporary_path, state_path)
except (OSError, TypeError, ValueError):
# Diagnostics must never make a hook invocation fail.
return True
return True
Loading
Loading