Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e2838d9
docs(rfc): propose handoff session rollover
AlexStocks Sep 29, 2026
e1dd12e
docs(rfc): assign handoff rollover number
AlexStocks Sep 29, 2026
5e0f8ce
feat(handoff): support session rollover reasons
AlexStocks Sep 29, 2026
7814af3
docs(rfcs): define host rollover sequence, trigger policy, and receiv…
AlexStocks Oct 10, 2026
fbd3fa7
Merge master into codex/handoff-session-rollover-rfc
AlexStocks Oct 10, 2026
4fde3a6
fix(contract): regenerate dify, desktop, and skill-up artifacts for r…
AlexStocks Oct 10, 2026
1508074
fix(handoff): preserve rollover invariants
AlexStocks Oct 11, 2026
4e0a461
fix(handoff): validate rollover prompt demonstrations
AlexStocks Oct 11, 2026
a3633d8
Merge remote-tracking branch 'origin/master' into codex/pr-1783-merge…
AlexStocks Oct 11, 2026
7180bec
Merge branch 'master' into codex/handoff-session-rollover-rfc
AlexStocks Oct 12, 2026
22b56b5
feat(artifacts): enforce the rollover quality floor in handoff prepare
AlexStocks Oct 12, 2026
fd858c9
feat(integrations): forward Handoff rollover reasons
AlexStocks Oct 12, 2026
101730b
fix(dsh): omit unsupported Handoff schema keyword
AlexStocks Oct 12, 2026
91eaf4c
fix(handoff): reject transcript-dependent rollover actions
AlexStocks Oct 12, 2026
8907d30
Merge remote-tracking branch 'origin/master' into codex/pr-1783-merge…
AlexStocks Oct 13, 2026
96653f6
fix(opencode): regenerate operation tables
AlexStocks Oct 13, 2026
9e427df
Merge remote-tracking branch 'origin/master' into codex/pr-1783-merge…
AlexStocks Oct 13, 2026
73bc9b7
fix(handoff): validate rollover demonstrations
AlexStocks Oct 13, 2026
3699ac3
fix(zcode): refresh guidance skill pin
AlexStocks Oct 13, 2026
c555f73
Merge remote-tracking branch 'origin/master' into codex/pr-1783-merge…
AlexStocks Oct 13, 2026
c293286
fix(handoff): allow descriptive rollover references
AlexStocks Oct 13, 2026
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
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/transport/operations.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"contractSha256": "fdc752737cc446c96a1482debcde927860e1ce30539b7a647a78c8a64f94b112",
"contractSha256": "5c00096f12642263d6bc538f7271a1484e1f8a17660786b12563cac9f4d6b0ab",
"operations": {
"get_liveness": {
"method": "GET",
Expand Down
6 changes: 6 additions & 0 deletions desktop/ui/src/generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2379,6 +2379,8 @@ export interface components {
boundary_source: components["schemas"]["SourceReference"];
objective: string;
/** @default [] */
rollover_reasons: components["schemas"]["HandoffRolloverReason"][];
/** @default [] */
evidence: components["schemas"]["HandoffCitation"][];
/** @default 8000 */
max_bytes: number;
Expand Down Expand Up @@ -3241,6 +3243,8 @@ export interface components {
PrepareHandoffRequest: {
scope_id: string;
objective: string;
/** @default [] */
rollover_reasons: components["schemas"]["HandoffRolloverReason"][];
evidence: components["schemas"]["HandoffCitation"][];
/** @default 8000 */
max_bytes: number;
Expand Down Expand Up @@ -4532,6 +4536,8 @@ export interface components {
/** @enum {string} */
HandoffResolutionStatus: "empty" | "resolved";
/** @enum {string} */
HandoffRolloverReason: "user_requested" | "host_context_budget" | "host_compaction" | "context_quality" | "delegation" | "manual_checkpoint";
/** @enum {string} */
HandoffSchema: "powercontext.handoff.v1";
/** @enum {string} */
HandoffSelection: "prepared" | "exact" | "latest";
Expand Down
2 changes: 1 addition & 1 deletion desktop/ui/src/generated/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

// Generated from openapi/powercontext.yaml. Do not edit.
export const contractSha256 = "fdc752737cc446c96a1482debcde927860e1ce30539b7a647a78c8a64f94b112";
export const contractSha256 = "5c00096f12642263d6bc538f7271a1484e1f8a17660786b12563cac9f4d6b0ab";
export const operations = {
"get_liveness": {
"method": "GET",
Expand Down
342 changes: 342 additions & 0 deletions docs/en/rfcs/1783-handoff-session-rollover.md

Large diffs are not rendered by default.

342 changes: 342 additions & 0 deletions docs/zh/rfcs/1783-handoff-session-rollover.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ not replace those schemas or enforce their constraints. It contains no scenario
## mcp__powercontext__activate_handoff

- Required: `boundary_source: SourceReference`, `objective: string`, `scope_id: string`.
- Optional: `evidence: array<HandoffCitation> = []`, `max_bytes: integer = 8000`.
- Optional: `evidence: array<HandoffCitation> = []`, `max_bytes: integer = 8000`, `rollover_reasons: array<"user_requested" | "host_context_budget" | "host_compaction" | "context_quality" | "delegation" | "manual_checkpoint"> = []`.

## mcp__powercontext__approve_artifact_candidate

Expand Down
4 changes: 4 additions & 0 deletions integrations/dify/generate_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
"family": ("候选类型", "Optional experience/skill filter; omission keeps the Server's unfiltered listing."),
"cursor": ("分页游标", "Complete next_cursor from the previous candidate page; do not invent or rewrite it."),
"candidate_id": ("候选 ID", "Exact candidate ID returned by generation or candidate listing."),
"rollover_reasons": (
"轮换原因",
"Optional advisory rollover reasons for this preparation; they do not authorize commit or execution.",
),
}
OUTPUT: dict[str, Any] = {
"type": "object",
Expand Down
31 changes: 31 additions & 0 deletions integrations/dify/plugin/powercontext_dify/contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -562,9 +562,11 @@
],
"activate_handoff": [
"boundary_source",
"rollover_reasons",
"evidence"
],
"prepare_handoff": [
"rollover_reasons",
"evidence"
],
"finalize_handoff": [
Expand Down Expand Up @@ -846,6 +848,15 @@
"maxLength": 8192,
"pattern": ".*\\S.*"
},
"rollover_reasons": {
"type": "array",
"maxItems": 6,
"uniqueItems": true,
"items": {
"$ref": "#/components/schemas/HandoffRolloverReason"
},
"default": []
},
"evidence": {
"type": "array",
"maxItems": 32,
Expand Down Expand Up @@ -2500,6 +2511,17 @@
"resolved"
]
},
"HandoffRolloverReason": {
"type": "string",
"enum": [
"user_requested",
"host_context_budget",
"host_compaction",
"context_quality",
"delegation",
"manual_checkpoint"
]
},
"HandoffSchema": {
"type": "string",
"enum": [
Expand Down Expand Up @@ -2881,6 +2903,15 @@
"maxLength": 8192,
"pattern": ".*\\S.*"
},
"rollover_reasons": {
"type": "array",
"maxItems": 6,
"uniqueItems": true,
"items": {
"$ref": "#/components/schemas/HandoffRolloverReason"
},
"default": []
},
"evidence": {
"type": "array",
"minItems": 1,
Expand Down
19 changes: 19 additions & 0 deletions integrations/dify/plugin/tools/pc_handoff_activate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ parameters:
en_US: The work objective to continue with the selected evidence.
zh_Hans: 交接目标;按工具契约保留完整值。
llm_description: The work objective to continue with the selected evidence.
- name: rollover_reasons
type: string
required: false
form: llm
label:
en_US: rollover_reasons
zh_Hans: 轮换原因
human_description:
en_US: Optional advisory rollover reasons for this preparation; they do not authorize
commit or execution. Supply one JSON-encoded text value. Strings include JSON
quotes; null is the text null. Omit unused optional inputs and preserve every
field.
zh_Hans: 轮换原因;填写一次 JSON 序列化后的文本;字符串须带 JSON 双引号,空值填 null;可选参数不用时省略,保留所有字段。
llm_description: 'Optional advisory rollover reasons for this preparation; they
do not authorize commit or execution. 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: {"type":"array","maxItems":6,"uniqueItems":true,"items":{"type":"string","enum":["user_requested","host_context_budget","host_compaction","context_quality","delegation","manual_checkpoint"]},"default":[]}'
default: '[]'
- name: evidence
type: string
required: false
Expand Down
19 changes: 19 additions & 0 deletions integrations/dify/plugin/tools/pc_handoff_prepare.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ parameters:
en_US: The work objective to continue with the selected evidence.
zh_Hans: 交接目标;按工具契约保留完整值。
llm_description: The work objective to continue with the selected evidence.
- name: rollover_reasons
type: string
required: false
form: llm
label:
en_US: rollover_reasons
zh_Hans: 轮换原因
human_description:
en_US: Optional advisory rollover reasons for this preparation; they do not authorize
commit or execution. Supply one JSON-encoded text value. Strings include JSON
quotes; null is the text null. Omit unused optional inputs and preserve every
field.
zh_Hans: 轮换原因;填写一次 JSON 序列化后的文本;字符串须带 JSON 双引号,空值填 null;可选参数不用时省略,保留所有字段。
llm_description: 'Optional advisory rollover reasons for this preparation; they
do not authorize commit or execution. 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: {"type":"array","maxItems":6,"uniqueItems":true,"items":{"type":"string","enum":["user_requested","host_context_budget","host_compaction","context_quality","delegation","manual_checkpoint"]},"default":[]}'
default: '[]'
- name: evidence
type: string
required: true
Expand Down
10 changes: 10 additions & 0 deletions integrations/dify/tests/test_contract_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ def test_unrelated_openapi_changes_leave_all_dify_outputs_unchanged(repository:
assert generated_files(repository) == before


def test_rollover_reasons_has_a_chinese_parameter_label(repository: Path) -> None:
declaration = yaml.safe_load(
(repository / "integrations/dify/plugin/tools/pc_handoff_prepare.yaml").read_text(encoding="utf-8")
)

parameter = next(item for item in declaration["parameters"] if item["name"] == "rollover_reasons")

assert parameter["label"]["zh_Hans"] == "轮换原因"


def test_consumed_schema_changes_fail_stale_checks_and_update_outputs(repository: Path) -> None:
before = generated_files(repository)
contract = repository / "openapi/powercontext.yaml"
Expand Down
26 changes: 22 additions & 4 deletions integrations/dsh/plugins/powercontext/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4041,6 +4041,20 @@ const HANDOFF_EVIDENCE = {
},
description: "For captured evidence use {kind: \"source\", source_ref: data.source}, copying the exact result. No raw facts."
};
const HANDOFF_ROLLOVER_REASONS = {
type: "array",
items: {
type: "string",
enum: [
"user_requested",
"host_context_budget",
"host_compaction",
"context_quality",
"delegation",
"manual_checkpoint"
]
}
};
function handoffTools(runtime, defineTool) {
return [
pcTool(defineTool, {
Expand All @@ -4059,12 +4073,14 @@ function handoffTools(runtime, defineTool) {
evidence: {
type: "array",
items: HANDOFF_EVIDENCE
}
},
rollover_reasons: HANDOFF_ROLLOVER_REASONS
},
execute: (args, exec) => run(runtime, exec, "activate_handoff", {
boundary_source: args.boundary_source,
objective: args.objective,
evidence: args.evidence ?? []
evidence: args.evidence ?? [],
...args.rollover_reasons === void 0 ? {} : { rollover_reasons: args.rollover_reasons }
})
}),
pcTool(defineTool, {
Expand All @@ -4080,11 +4096,13 @@ function handoffTools(runtime, defineTool) {
type: "array",
required: true,
items: HANDOFF_EVIDENCE
}
},
rollover_reasons: HANDOFF_ROLLOVER_REASONS
},
execute: (args, exec) => run(runtime, exec, "prepare_handoff", {
objective: args.objective,
evidence: args.evidence
evidence: args.evidence,
...args.rollover_reasons === void 0 ? {} : { rollover_reasons: args.rollover_reasons }
})
}),
pcTool(defineTool, {
Expand Down
16 changes: 15 additions & 1 deletion integrations/dsh/plugins/powercontext/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,13 @@ const HANDOFF_EVIDENCE = {
},
description: 'For captured evidence use {kind: "source", source_ref: data.source}, copying the exact result. No raw facts.',
}
const HANDOFF_ROLLOVER_REASONS = {
type: 'array',
items: {
type: 'string',
enum: ['user_requested', 'host_context_budget', 'host_compaction', 'context_quality', 'delegation', 'manual_checkpoint'],
},
}

function handoffTools(runtime: PluginRuntime, defineTool: DefineTool): unknown[] {
return [
Expand All @@ -268,9 +275,11 @@ function handoffTools(runtime: PluginRuntime, defineTool: DefineTool): unknown[]
boundary_source: { ...SOURCE_REFERENCE, required: true },
objective: { type: 'string', required: true },
evidence: { type: 'array', items: HANDOFF_EVIDENCE },
rollover_reasons: HANDOFF_ROLLOVER_REASONS,
},
execute: (args, exec) => run(runtime, exec, 'activate_handoff', {
boundary_source: args.boundary_source, objective: args.objective, evidence: args.evidence ?? [],
...(args.rollover_reasons === undefined ? {} : { rollover_reasons: args.rollover_reasons }),
}),
}),
pcTool(defineTool, {
Expand All @@ -285,8 +294,13 @@ function handoffTools(runtime: PluginRuntime, defineTool: DefineTool): unknown[]
parameters: {
objective: { type: 'string', required: true },
evidence: { type: 'array', required: true, items: HANDOFF_EVIDENCE },
rollover_reasons: HANDOFF_ROLLOVER_REASONS,
},
execute: (args, exec) => run(runtime, exec, 'prepare_handoff', { objective: args.objective, evidence: args.evidence }),
execute: (args, exec) => run(runtime, exec, 'prepare_handoff', {
objective: args.objective,
evidence: args.evidence,
...(args.rollover_reasons === undefined ? {} : { rollover_reasons: args.rollover_reasons }),
}),
}),
pcTool(defineTool, {
name: 'pc_handoff_finalize',
Expand Down
69 changes: 69 additions & 0 deletions integrations/dsh/plugins/powercontext/tests/tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { describe, expect, it, vi } from 'vitest'
import { PowerContextClient } from '../src/client.ts'
import { registerTools } from '../src/tools.ts'
import type { PluginRuntime } from '../src/invoke.ts'

Expand Down Expand Up @@ -127,4 +128,72 @@ describe('agent tool surface', () => {
code: 'unscoped',
})
})

it('forwards advisory rollover reasons through both Handoff draft operations', async () => {
const registered: Array<Record<string, unknown>> = []
const requests: Array<{ url: string; body: Record<string, unknown> }> = []
const runtime = {
client: new PowerContextClient({
baseUrl: 'http://127.0.0.1:8000',
allowInsecureHttp: true,
requestTimeoutMs: 1000,
fetch: async (url, init) => {
requests.push({ url, body: JSON.parse(String(init.body)) })
return Response.json({ ok: true })
},
}),
config: { maxBytes: 8000 },
resolveScope: vi.fn(async () => 'project:demo'),
log: vi.fn(),
} as unknown as PluginRuntime
registerTools(
{ tools: { register: (tool) => registered.push(tool as Record<string, unknown>) }, on: () => undefined },
runtime,
(definition) => definition,
)
const activate = registered.find((tool) => tool.name === 'pc_handoff_activate') as {
parameters: Record<string, unknown>
execute: (args: Record<string, unknown>, exec: { signal: AbortSignal }) => Promise<unknown>
}
const prepare = registered.find((tool) => tool.name === 'pc_handoff_prepare') as typeof activate
const boundarySource = { name: 'content', source_id: 'turn-1' }
const reasons = ['host_context_budget', 'context_quality']

expect(activate.parameters.rollover_reasons).toMatchObject({
type: 'array',
items: { type: 'string', enum: ['user_requested', 'host_context_budget', 'host_compaction', 'context_quality', 'delegation', 'manual_checkpoint'] },
})
expect(prepare.parameters.rollover_reasons).toMatchObject({
type: 'array',
items: { type: 'string', enum: ['user_requested', 'host_context_budget', 'host_compaction', 'context_quality', 'delegation', 'manual_checkpoint'] },
})
expect(activate.parameters.rollover_reasons).not.toHaveProperty('maxItems')
expect(prepare.parameters.rollover_reasons).not.toHaveProperty('maxItems')
await activate.execute({
boundary_source: boundarySource,
objective: 'Continue in a fresh session.',
rollover_reasons: reasons,
}, { signal: new AbortController().signal })
await prepare.execute({
objective: 'Continue in a fresh session.',
evidence: [{ kind: 'source', source_ref: boundarySource }],
rollover_reasons: reasons,
}, { signal: new AbortController().signal })

expect(requests).toEqual([
{ url: 'http://127.0.0.1:8000/v1/handoff/activate', body: {
boundary_source: boundarySource,
objective: 'Continue in a fresh session.',
evidence: [],
rollover_reasons: reasons,
scope_id: 'project:demo',
} },
{ url: 'http://127.0.0.1:8000/v1/handoff/prepare', body: {
objective: 'Continue in a fresh session.',
evidence: [{ kind: 'source', source_ref: boundarySource }],
rollover_reasons: reasons,
scope_id: 'project:demo',
} },
])
})
})
Loading
Loading