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
67 changes: 67 additions & 0 deletions desktop/ui/src/generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/sources/working-notes": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Capture session-local working evidence
* @description Explicitly capture a short-lived Working Note as Source evidence. Use this only when the user or host flow asks to retain intermediate work state such as inspected files, an abandoned approach, a validation result, an unresolved question, or host context. The note is untrusted historical material, not an instruction or a durable Memory entry. It is excluded from automatic Artifact generation and can support a Handoff only through an exact Source citation. Capture does not end the session, expire older notes, or promote the note into Memory. An exact replay of an accepted capture remains idempotent after its logical expiry.
*/
post: operations["capture_working_note"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/source-definitions/register": {
parameters: {
query?: never;
Expand Down Expand Up @@ -3205,10 +3225,26 @@ export interface components {
source_id: string;
/** @description Raw integration content. Server-reserved payload schemas, including handoff receipts, are rejected on this generic capture operation and must be created through their dedicated workflow. */
content: string;
/** @description Caller metadata. The reserved kind value `working_note` is rejected; use the dedicated Working Note capture operation instead. */
metadata?: {
[key: string]: unknown;
} | null;
};
/** @enum {string} */
WorkingNoteKind: "inspection" | "abandoned_approach" | "validation" | "unresolved" | "host_context";
CaptureWorkingNoteRequest: {
scope_id: string;
source_id: string;
content: string;
/** @description Host/session correlation only; it is not an authorization boundary. */
session_id: string;
note_kind: components["schemas"]["WorkingNoteKind"];
/**
* Format: date-time
* @description Optional logical expiry. It must be in the future when captured. Expiry makes the note unavailable to new Handoff evidence resolution but does not physically delete or rewrite the Source.
*/
expires_at?: string | null;
};
CaptureContentSourceResponse: {
status: components["schemas"]["CaptureStatus"];
source: components["schemas"]["SourceReference"];
Expand Down Expand Up @@ -5985,6 +6021,37 @@ export interface operations {
503: components["responses"]["Unavailable"];
};
};
capture_working_note: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["CaptureWorkingNoteRequest"];
};
};
responses: {
/** @description The Working Note is durably stored as Source evidence. */
202: {
headers: {
"X-PowerContext-Request-ID": components["headers"]["RequestId"];
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CaptureContentSourceResponse"];
};
};
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
409: components["responses"]["Conflict"];
422: components["responses"]["InvalidRequest"];
500: components["responses"]["InternalError"];
503: components["responses"]["Unavailable"];
};
};
register_source_definition: {
parameters: {
query?: never;
Expand Down
60 changes: 60 additions & 0 deletions docs/en/rfcs/1790_source_backed_working_notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Source-backed Working Notes

- Proposal Name: `source_backed_working_notes`
- Tracking Issue: [oceanbase/powercontext#1790](https://github.com/oceanbase/powercontext/issues/1790)
- Status: implementation decision

## Decision

PowerContext uses a Source-kind-only design for short-lived working notes. It does not add a durable `notes` Artifact
family. A working note is captured as a `ContentSource` marked by a server-owned field, with descriptive kind and
session metadata. The Source remains addressable by its exact `(scope_id, source_type, source_id)` identity.

This is distinct from a Memory entry whose `kind` is also `working_note`: the latter is an explicit Memory write and is
eligible for Memory lifecycle and retrieval. A captured Working Note is evidence only until a caller explicitly uses it
in a domain operation.

Hosts capture one through `POST /v1/sources/working-notes`, the generated Python Client's `capture_working_note`, or the
same MCP tool when exposed. The request requires a caller-stable `source_id`, host `session_id`, content, and one of
`inspection`, `abandoned_approach`, `validation`, `unresolved`, or `host_context`. Replaying an identity with different
content remains a Source conflict. A caller may also set a timezone-aware `expires_at` in the future. An exact replay
of an accepted capture returns its original receipt even after expiry; expiry does not weaken capture idempotency.

Working Note classification is a server-owned `ContentSource` field. The `metadata.kind=working_note` value is retained
as descriptive metadata but is not authoritative by itself. Generic content capture rejects that reserved kind and
cannot set the server-owned field. Existing Sources that only happen to carry this metadata value remain ordinary
Sources after upgrade and retain their previous generation behavior.

## Processing boundary

Working Notes are excluded from automatic Source-window generation. They must not be promoted into Memory, Topic Memory,
Experience, or another generated artifact merely because they were captured. Explicit consumers may opt in to the exact
Source reference. Handoff evidence resolution opts in because a cited Working Note is still inspectable evidence.

The content is untrusted historical material. It is not an instruction, Skill, prompt policy, or executable tool input.

## Retention and access

Working Notes inherit Scope Source authorization. `session_id` identifies the originating host/session for grouping and
future cleanup; it is not an additional authorization boundary. When `expires_at` is omitted, the note remains active
until a future explicit lifecycle action. After the timestamp, new consumers, including Handoff evidence resolution,
must treat the note as unavailable.

Expiry is logical: it does not delete or rewrite the immutable Source, which remains readable through the supported
Source audit path. Physical purge, session-end cleanup, legal hold, and cross-scope export are separate lifecycle work
and are not implemented by this decision.

An exact Source reference remains the evidence identity when a Handoff cites a note. A later cleanup operation must not
silently resolve that citation to different content.

## Promotion

Promotion is disabled in the initial implementation. A future explicit promotion operation must submit the exact
Source reference and pass the owning domain's normal validation. No extractor may infer a durable `fact`, `decision`,
or `constraint` solely from the note's presence.

## Alternatives rejected

- A new Notes Artifact family duplicates Handoff and Source revision/lineage machinery before the use case is proven.
- Storing notes directly in Memory pollutes durable retrieval with session progress.
- Keeping notes only in host transcripts prevents cross-host evidence resolution.
53 changes: 53 additions & 0 deletions docs/zh/rfcs/1790_source_backed_working_notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 基于 Source 的 Working Note

- 提案名称:`source_backed_working_notes`
- 跟踪 Issue:[oceanbase/powercontext#1790](https://github.com/oceanbase/powercontext/issues/1790)
- 状态:实现决策

## 决策

PowerContext 对短期工作记录采用仅增加 Source kind 的方案,不新增持久化的 `notes` Artifact family。Working Note
保存为由服务端字段标记的 `ContentSource`,并携带描述性的 session 和 note kind 元数据。它仍然通过完整的
`(scope_id, source_type, source_id)` 身份寻址。

这与 Memory 中 `kind=working_note` 的 Entry 不同:后者是显式 Memory 写入,会进入 Memory 生命周期和检索;捕获的
Working Note 只是 evidence,除非调用方显式把它用于领域操作。

宿主可以通过 `POST /v1/sources/working-notes`、生成的 Python Client `capture_working_note`,或启用时的同名 MCP tool
完成捕获。请求必须包含调用方稳定的 `source_id`、宿主 `session_id`、正文,以及 `inspection`、
`abandoned_approach`、`validation`、`unresolved` 或 `host_context` 之一。使用相同 identity 重放不同内容仍然属于
Source conflict。调用方也可以设置一个带时区且晚于当前时间的 `expires_at`。已接受捕获的完全相同重放即使发生在
过期后,也会返回原 receipt;过期不会削弱捕获幂等性。

Working Note 分类由服务端拥有的 `ContentSource` 字段决定。`metadata.kind=working_note` 仍作为描述性元数据保留,
但它本身不是权威标记。通用 content capture 会拒绝这个保留 kind,也无法设置服务端字段。升级前仅碰巧携带该
metadata 值的 Source 仍按普通 Source 处理,并保持原有 generation 行为。

## 处理边界

Working Note 不进入自动 Source-window generation。仅仅捕获它不能触发 Memory、Topic Memory、Experience 或其他
生成 Artifact。显式消费者可以选择精确的 Source 引用;Handoff evidence resolution 会显式允许这一用途。

Working Note 内容是不可信的历史材料,不是指令、Skill、prompt policy 或可执行工具输入。

## 保留与访问

Working Note 继承 Scope Source 的授权。`session_id` 只用于按宿主/session 分组和未来清理,不构成额外授权边界。
未设置 `expires_at` 时,Note 会保持有效,直到未来显式生命周期操作改变它;时间到期后,包括 Handoff evidence
resolution 在内的新消费者必须将其视为不可用。

这里的过期是逻辑过期:它不会删除或改写不可变 Source,Source 仍可通过受支持的审计读取路径访问。物理清理、
session 结束时清理、legal hold 和跨 Scope 导出属于独立的后续生命周期工作,不在本决策的实现范围内。

Handoff 引用记录的始终是精确 Source identity。后续清理不得把该引用静默解析为另一份内容。

## 晋升

首期实现关闭晋升。未来的显式晋升操作必须提交精确 Source 引用,并通过所属领域的现有校验。不能仅因为 Note 存在,
就自动推断出 durable `fact`、`decision` 或 `constraint`。

## 不采用的替代方案

- 立即新增 Notes Artifact family:在场景验证前复制 Handoff 和 Source 的 revision/lineage 机制。
- 直接写入 Memory:会把 session 进度污染到持久检索表面。
- 只保存在宿主 transcript:无法跨宿主进行 evidence 解析。
2 changes: 1 addition & 1 deletion evaluation/skills/skill-up/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ All relative config paths start at this project root, not at `evals/`.

Both arms receive the same `evals/fixtures/repos/powercontext-tool-contract/CLAUDE.md`: required/optional top-level
argument names, types and defaults derived from OpenAPI and its MCP specialization, including content CAS headers.
The mock catalog contains the Server's complete declared MCP tool subset (54 operations), verified against
The mock catalog contains the Server's complete declared MCP tool subset (55 operations), verified against
`src/powercontext/server/mcp.py` and the generated operation IDs. The successful save reply contains Atomic
`records`; empty search returns the current `text` mode and an empty `hits` list.
This fills the generic mock schemas' parameter information gap
Expand Down
2 changes: 2 additions & 0 deletions evaluation/skills/skill-up/evals/cases/empty-search.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ judge:
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__activate_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__approve_candidate}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_content_source}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_working_note}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__clear_scope_binding}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__commit_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__continue_handoff}
Expand Down Expand Up @@ -80,6 +81,7 @@ judge:
- tool_not_called_in_turn: {turn: 2, name: mcp__powercontext__activate_handoff}
- tool_not_called_in_turn: {turn: 2, name: mcp__powercontext__approve_candidate}
- tool_not_called_in_turn: {turn: 2, name: mcp__powercontext__capture_content_source}
- tool_not_called_in_turn: {turn: 2, name: mcp__powercontext__capture_working_note}
- tool_not_called_in_turn: {turn: 2, name: mcp__powercontext__clear_scope_binding}
- tool_not_called_in_turn: {turn: 2, name: mcp__powercontext__commit_handoff}
- tool_not_called_in_turn: {turn: 2, name: mcp__powercontext__continue_handoff}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ judge:
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__activate_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__approve_candidate}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_content_source}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_working_note}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__clear_scope_binding}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__commit_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__continue_handoff}
Expand Down
1 change: 1 addition & 0 deletions evaluation/skills/skill-up/evals/cases/failed-save.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ judge:
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__activate_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__approve_candidate}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_content_source}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_working_note}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__clear_scope_binding}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__commit_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__continue_handoff}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ judge:
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__activate_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__approve_candidate}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_content_source}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_working_note}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__clear_scope_binding}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__commit_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__continue_handoff}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ judge:
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__activate_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__approve_candidate}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_content_source}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__capture_working_note}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__clear_scope_binding}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__commit_handoff}
- tool_not_called_in_turn: {turn: 1, name: mcp__powercontext__continue_handoff}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"capture_content_source": {
"error": "unexpected"
},
"capture_working_note": {
"error": "unexpected"
},
"change_atomic_memory_lifecycle": {
"error": "unexpected"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"capture_content_source": {
"error": "unexpected"
},
"capture_working_note": {
"error": "unexpected"
},
"change_atomic_memory_lifecycle": {
"error": "unexpected"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ not replace those schemas or enforce their constraints. It contains no scenario
- Required: `content: string`, `scope_id: string`, `source_id: string`.
- Optional: `metadata: object | null`.

## mcp__powercontext__capture_working_note

- Required: `content: string`, `note_kind: "inspection" | "abandoned_approach" | "validation" | "unresolved" | "host_context"`, `scope_id: string`, `session_id: string`, `source_id: string`.
- Optional: `expires_at: string | null`.

## mcp__powercontext__change_atomic_memory_lifecycle

- Required: `scope_id: string`, `state: "forgotten"`, `target: AtomicMemoryInput`.
Expand Down
9 changes: 9 additions & 0 deletions integrations/capabilities.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ capabilities = ["memory_write"]
id = "capture_content_source"
capabilities = ["source_capture"]
[[toolsets.tools]]
id = "capture_working_note"
capabilities = ["source_capture"]
[[toolsets.tools]]
id = "create_work_contract"
capabilities = ["work_contract"]
[[toolsets.tools]]
Expand Down Expand Up @@ -668,6 +671,9 @@ capabilities = ["context_injection"]
id = "pc_capture_source:capture_content_source"
capabilities = ["source_capture"]
[[toolsets.tools]]
id = "pc_capture_working_note:capture_working_note"
capabilities = ["source_capture"]
[[toolsets.tools]]
id = "pc_handoff_activate:activate_handoff"
capabilities = ["handoff"]
[[toolsets.tools]]
Expand Down Expand Up @@ -735,6 +741,9 @@ capabilities = ["context_injection"]
id = "pc_capture_source:capture_content_source"
capabilities = ["source_capture"]
[[toolsets.tools]]
id = "pc_capture_working_note:capture_working_note"
capabilities = ["source_capture"]
[[toolsets.tools]]
id = "pc_handoff_activate:activate_handoff"
capabilities = ["handoff"]
[[toolsets.tools]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"approve_candidate",
"capture_content_source",
"change_atomic_memory_lifecycle",
"capture_working_note",
"commit_handoff",
"continue_handoff",
"create_remote_skill_target",
Expand Down
3 changes: 2 additions & 1 deletion integrations/dify/plugin/powercontext_dify/contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -1561,7 +1561,8 @@
"anyOf": [
{
"type": "object",
"additionalProperties": true
"additionalProperties": true,
"description": "Caller metadata. The reserved kind value `working_note` is rejected; use the dedicated Working Note capture operation instead."
},
{
"type": "null"
Expand Down
4 changes: 3 additions & 1 deletion integrations/dify/plugin/tools/pc_capture_source.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ parameters:
to dify. Pass one JSON-encoded value as text, never a native object/array/null.
Encode strings with JSON quotes; use the text null for null; omit optional inputs
when unused. Decode exactly once and preserve every field. Decoded JSON Schema:
{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}]}'
{"anyOf":[{"type":"object","additionalProperties":true,"description":"Caller metadata.
The reserved kind value `working_note` is rejected; use the dedicated Working
Note capture operation instead."},{"type":"null"}]}'
output_schema:
type: object
properties:
Expand Down
Loading