Skip to content
Closed
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
15844db
feat(coding-agent): add host request contracts
sethkarten Aug 11, 2026
bebccac
fix(coding-agent): retain root kill cleanup ownership
sethkarten Aug 11, 2026
2ff920d
chore(coding-agent): restore pre-formatter root kill test layout
sethkarten Aug 11, 2026
f6f91a2
style(coding-agent): manually format root kill regression
sethkarten Aug 11, 2026
6ffa48e
chore(coding-agent): record unsigned remediation acceptance
sethkarten Aug 11, 2026
6542032
feat(kernel): dispatch host requests as capabilities
sethkarten Aug 11, 2026
f8b51e4
fix(kernel): keep host request authority dispatcher-private
sethkarten Aug 11, 2026
76f6fb3
fix(kernel): revoke retained host request contexts
sethkarten Aug 11, 2026
f3ba1a1
fix(kernel): close host request admission during shutdown
sethkarten Aug 11, 2026
6c02719
fix(kernel): reopen host request admission on restart
sethkarten Aug 11, 2026
3add1b3
fix(kernel): prevent terminal lifecycle restart race
sethkarten Aug 11, 2026
685e1a6
fix(kernel): satisfy startup terminal type guard
sethkarten Aug 11, 2026
3c3b0d2
fix(kernel): keep startup retries nonterminal
sethkarten Aug 11, 2026
b12f647
fix(kernel): reopen host requests for startup retry
sethkarten Aug 11, 2026
0193a1e
fix(kernel): suppress stale closed-comm replies
sethkarten Aug 11, 2026
2b782fa
fix(kernel): preserve diagnostics without stale replies
sethkarten Aug 11, 2026
3c3dd0d
feat(daemon): reconstruct managed session catalog
sethkarten Aug 11, 2026
b8f2c86
fix(daemon): preserve CLI sender relationship fallback
sethkarten Aug 12, 2026
15b56d0
feat(coding-agent): add host request contracts
sethkarten Aug 11, 2026
200661c
fix(coding-agent): retain root kill cleanup ownership
sethkarten Aug 11, 2026
7e0333d
chore(coding-agent): restore pre-formatter root kill test layout
sethkarten Aug 11, 2026
1374c91
style(coding-agent): manually format root kill regression
sethkarten Aug 11, 2026
6cd8277
chore(coding-agent): record unsigned remediation acceptance
sethkarten Aug 11, 2026
5edc39a
feat(kernel): dispatch host requests as capabilities
sethkarten Aug 11, 2026
aa21a39
fix(kernel): keep host request authority dispatcher-private
sethkarten Aug 11, 2026
9e6d5ee
fix(kernel): revoke retained host request contexts
sethkarten Aug 11, 2026
5449ca9
fix(kernel): close host request admission during shutdown
sethkarten Aug 11, 2026
2feb901
fix(kernel): reopen host request admission on restart
sethkarten Aug 11, 2026
15b6bb9
fix(kernel): prevent terminal lifecycle restart race
sethkarten Aug 11, 2026
4a91eb2
fix(kernel): satisfy startup terminal type guard
sethkarten Aug 11, 2026
5b8433a
fix(kernel): keep startup retries nonterminal
sethkarten Aug 11, 2026
9b0235b
fix(kernel): reopen host requests for startup retry
sethkarten Aug 11, 2026
285ac85
fix(kernel): suppress stale closed-comm replies
sethkarten Aug 11, 2026
94e98f7
fix(kernel): preserve diagnostics without stale replies
sethkarten Aug 11, 2026
d56f609
fix(kernel): snapshot before terminal teardown
sethkarten Aug 12, 2026
6e2af8a
Merge commit 'c2f3e01072fa8dac596eac1f1c1b99a35c85394e' into core03-m…
sethkarten Aug 12, 2026
e95e0a8
fix(daemon): include artifact family topology
sethkarten Aug 12, 2026
fc18c2c
fix(daemon): bind artifact family authority
sethkarten Aug 12, 2026
870d57b
fix(daemon): bind catalog reads to authority descriptors
sethkarten Aug 12, 2026
1e78f19
test(daemon): stub immutable family catalog
sethkarten Aug 12, 2026
4f87ab5
fix(daemon): close catalog authority topology gaps
sethkarten Aug 12, 2026
4ac5fc9
test(kernel): isolate forkserver setting in startup tests
sethkarten Aug 12, 2026
d685ec4
Merge updated core02 parent into core03
sethkarten Aug 12, 2026
061955b
fix(daemon): preserve custom session catalog authority
sethkarten Aug 12, 2026
3bc6523
test(daemon): expect saved session directory authority
sethkarten Aug 12, 2026
1c24e46
feat(coding-agent): add host request contracts
sethkarten Aug 11, 2026
4fec328
fix(coding-agent): retain root kill cleanup ownership
sethkarten Aug 11, 2026
6e3df3e
chore(coding-agent): restore pre-formatter root kill test layout
sethkarten Aug 11, 2026
313a481
style(coding-agent): manually format root kill regression
sethkarten Aug 11, 2026
b9454eb
Merge branch 'core01-root-kill-cleanup' into core02-host-request-disp…
sethkarten Aug 12, 2026
0f051bb
fix(coding-agent): isolate host reply send failures
sethkarten Aug 12, 2026
2311b76
Merge core02 host dispatcher into core03 session catalog
sethkarten Aug 12, 2026
5b96482
fix(daemon): use configured session directory for catalog authority
sethkarten Aug 12, 2026
03ac688
fix(daemon): label CLI sibling messages from catalog
sethkarten Aug 12, 2026
8a3c33f
fix(daemon): keep CLI message labels advisory
sethkarten Aug 12, 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
91 changes: 50 additions & 41 deletions packages/coding-agent/src/core/agent-messages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { randomUUID } from "node:crypto";
import type { AgentMessage } from "@earendil-works/pi-agent-core";
import type { HostRequestHandler } from "./kernel/index.js";
import { contextAwareHostRequestHandler, createHostRequestHandler, type HostRequestHandler } from "./kernel/index.js";
import type { CustomMessage } from "./messages.js";
import { canonicalSessionPath } from "./session-lease.js";

Expand Down Expand Up @@ -257,72 +257,81 @@ function sameAgentSessionNameParent(
if (left.depth === 0 && right.depth === 0) {
return true;
}
return sameAgentFamilyParent(left, right, catalog);
if (sameAgentFamilyParent(left, right, catalog)) return true;

// A passive child can outlive the active parent row that would normally
// resolve its family edge. Name reservation must still protect that parent's
// sibling namespace, but this weaker direct-claim fallback is deliberately
// not used for family reach: reach continues to require an unambiguous,
// catalog-resolved parent.
if (left.depth !== right.depth || left.depth === 0) return false;
return (
(left.parentSessionId !== undefined && left.parentSessionId === right.parentSessionId) ||
(left.parentSessionPath !== undefined &&
right.parentSessionPath !== undefined &&
canonicalSessionPath(left.parentSessionPath) === canonicalSessionPath(right.parentSessionPath))
);
}

function sameAgentFamilyParent(
left: AgentSessionNameScope,
right: AgentSessionNameScope,
catalog: readonly AgentFamilyCatalogEntry[],
): boolean {
if (left.parentSessionPath !== undefined && left.parentSessionPath === right.parentSessionPath) {
return true;
}
if (left.parentSessionId !== undefined && left.parentSessionId === right.parentSessionId) {
return true;
}
const hasCatalogParentPair = (parentSessionId: string | undefined, parentSessionPath: string | undefined) =>
parentSessionId !== undefined &&
parentSessionPath !== undefined &&
catalog.some(
(entry) =>
(entry.id === parentSessionId && entry.sessionPath === parentSessionPath) ||
(entry.parentSessionId === parentSessionId && entry.parentSessionPath === parentSessionPath),
if (left.depth === 0 && right.depth === 0) {
return (
left.parentSessionId === undefined &&
left.parentSessionPath === undefined &&
right.parentSessionId === undefined &&
right.parentSessionPath === undefined
);
if (
hasCatalogParentPair(left.parentSessionId, right.parentSessionPath) ||
hasCatalogParentPair(right.parentSessionId, left.parentSessionPath)
) {
return true;
}
if (
left.depth === 0 &&
right.depth === 0 &&
left.parentSessionPath === undefined &&
right.parentSessionPath === undefined &&
left.parentSessionId === undefined &&
right.parentSessionId === undefined
) {
return true;
}
// Unresolved mixed identifiers stay unrelated to avoid false name conflicts across families.
return false;
if (left.depth !== right.depth || left.depth === 0) return false;
const parentFor = (child: AgentSessionNameScope) => {
const parents = catalog.filter((entry) => isAgentFamilyParent(entry, child));
return parents.length === 1 ? parents[0] : undefined;
};
const leftParent = parentFor(left);
const rightParent = parentFor(right);
return leftParent !== undefined && leftParent.id === rightParent?.id;
}

function isAgentFamilyParent(parent: AgentFamilyCatalogEntry, child: AgentFamilyCatalogEntry): boolean {
/**
* Validates one persisted parent edge. A child may supply either durable
* identifier, but when it supplies both they must identify this same direct
* parent. This keeps contradictory records from becoming relatives through
* whichever identifier happens to match.
*/
function isAgentFamilyParent(parent: AgentFamilyCatalogEntry, child: AgentSessionNameScope): boolean {
if (child.depth <= 0 || parent.depth !== child.depth - 1) return false;
const claimsId = child.parentSessionId !== undefined;
const claimsPath = child.parentSessionPath !== undefined;
return (
(child.parentSessionPath !== undefined && child.parentSessionPath === parent.sessionPath) ||
(child.parentSessionId !== undefined && child.parentSessionId === parent.id)
(claimsId || claimsPath) &&
(!claimsId || child.parentSessionId === parent.id) &&
(!claimsPath || child.parentSessionPath === parent.sessionPath)
);
}

/** Pure nuclear-family policy over persisted parent-edge snapshots. */
export function agentFamilyRelationship(
current: AgentFamilyCatalogEntry,
target: AgentFamilyCatalogEntry,
catalog: readonly AgentFamilyCatalogEntry[] = [current, target],
): AgentFamilyRelationship | undefined {
if (current.id === target.id) return undefined;
if (isAgentFamilyParent(target, current)) return "parent";
if (isAgentFamilyParent(current, target)) return "child";
if (current.depth === target.depth && sameAgentFamilyParent(current, target, [current, target])) return "sibling";
if (sameAgentFamilyParent(current, target, catalog)) return "sibling";
Comment thread
cursor[bot] marked this conversation as resolved.
return undefined;
}

export function assertAgentFamilyReach(
current: AgentFamilyCatalogEntry,
target: AgentFamilyCatalogEntry,
catalog?: readonly AgentFamilyCatalogEntry[],
): AgentFamilyRelationship {
const relationship = agentFamilyRelationship(current, target);
const relationship = agentFamilyRelationship(current, target, catalog);
if (!relationship) throw new Error(AGENT_FAMILY_REACH_ERROR);
return relationship;
}
Expand Down Expand Up @@ -526,11 +535,11 @@ export function createAgentMessageHostHandlers(
controller: Pick<AgentSessionMessageController, "roster" | "sendAgentMessage" | "awaitPendingChildPublication">,
): Record<string, HostRequestHandler> {
return {
"agent_message.list_agents": async () => {
"agent_message.list_agents": createHostRequestHandler(async (_payload, _context) => {
if (!controller.roster) throw new Error("agent family roster is not available in this session");
return (await controller.roster()) as unknown as Record<string, unknown>;
},
"agent_message.send": async (payload) => {
}, contextAwareHostRequestHandler),
"agent_message.send": createHostRequestHandler(async (payload, _context) => {
if (typeof payload.message !== "string") {
throw new Error("agent_message.send message must be a string");
}
Expand Down Expand Up @@ -602,7 +611,7 @@ export function createAgentMessageHostHandlers(
message: payload.message,
receiverRole: payload.receiver_role as AgentFamilyRelationship,
})) as unknown as Record<string, unknown>;
},
}, contextAwareHostRequestHandler),
};
}

Expand Down
24 changes: 12 additions & 12 deletions packages/coding-agent/src/core/agent-observe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AgentMessage } from "@earendil-works/pi-agent-core";
import { contextAwareHostRequestHandler, createHostRequestHandler, type HostRequestHandlers } from "./kernel/index.js";

export const AGENT_OBSERVE_SKILL_NAME = "agent-observe";
export const AGENT_OBSERVE_IMPORT_NAME = "agent_observe";
Expand Down Expand Up @@ -67,25 +68,24 @@ export interface AgentObserveController {
): AgentObserveRecentMessagesResult | Promise<AgentObserveRecentMessagesResult>;
}

export function createAgentObserveHostHandlers(controller: AgentObserveController) {
export function createAgentObserveHostHandlers(controller: AgentObserveController): HostRequestHandlers {
return {
"agent_observe.list": async () => controller.listAgents() as unknown as Record<string, unknown>,
"agent_observe.get": async (payload: Record<string, unknown> = {}) => {
if (typeof payload.target !== "string") {
throw new Error("agent_observe.get target must be a string");
}
"agent_observe.list": createHostRequestHandler(
async (_payload, _context) => controller.listAgents() as unknown as Record<string, unknown>,
contextAwareHostRequestHandler,
),
"agent_observe.get": createHostRequestHandler(async (payload, _context) => {
if (typeof payload.target !== "string") throw new Error("agent_observe.get target must be a string");
return (await controller.getAgent(payload.target)) as unknown as Record<string, unknown>;
},
"agent_observe.recent": async (payload: Record<string, unknown> = {}) => {
if (typeof payload.target !== "string") {
throw new Error("agent_observe.recent target must be a string");
}
}, contextAwareHostRequestHandler),
"agent_observe.recent": createHostRequestHandler(async (payload, _context) => {
if (typeof payload.target !== "string") throw new Error("agent_observe.recent target must be a string");
return (await controller.recentMessages({
target: payload.target,
limit: normalizeOptionalInteger(payload.limit, "agent_observe.recent limit"),
maxChars: normalizeOptionalInteger(payload.max_chars ?? payload.maxChars, "agent_observe.recent max_chars"),
})) as unknown as Record<string, unknown>;
},
}, contextAwareHostRequestHandler),
};
}

Expand Down
40 changes: 30 additions & 10 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,12 @@ import {
validateGoalBudget,
validateGoalObjective,
} from "./goals.js";
import type { HostRequestHandlers, KernelSentAgentMessage } from "./kernel/index.js";
import {
contextAwareHostRequestHandler,
createHostRequestHandler,
type HostRequestHandlers,
type KernelSentAgentMessage,
} from "./kernel/index.js";
import { type RestoreResult, snapshotPathIn } from "./kernel/state-snapshot.js";
import type { McpManager } from "./mcp/mcp-manager.js";
import {
Expand Down Expand Up @@ -8766,25 +8771,37 @@ export class AgentSession {
"rlm.find_models": createRlmFindModelsHostHandler((query, limit) => this.findRlmModels(query, limit)),
"rlm.list_subagents": createRlmListSubagentsHostHandler(() => this.listRlmSubagents()),
"rlm.delete_subagent": createRlmDeleteSubagentHostHandler((target) => this.deleteRlmSubagent(target)),
"model.info": async () => ({
id: this.model?.id ?? null,
provider: this.model?.provider ?? null,
input: this.model?.input ?? [],
}),
"model.info": createHostRequestHandler(
async (_payload, _context) => ({
id: this.model?.id ?? null,
provider: this.model?.provider ?? null,
input: this.model?.input ?? [],
}),
contextAwareHostRequestHandler,
),
};
if (this._includeGoals) {
for (const type of ["goal.get", "goal.create", "goal.complete"]) {
handlers[type] = async (payload) => this.handleGoalHostRequest(type, payload);
handlers[type] = createHostRequestHandler(
async (payload, _context) => this.handleGoalHostRequest(type, payload),
contextAwareHostRequestHandler,
);
}
}
if (this._includeCompactSkill) {
for (const type of ["compact.run", "compact.status"]) {
handlers[type] = async (payload) => this.handleCompactHostRequest(type, payload);
handlers[type] = createHostRequestHandler(
async (payload, _context) => this.handleCompactHostRequest(type, payload),
contextAwareHostRequestHandler,
);
}
}
if (this._autoRefineAllowedForSession()) {
for (const type of ["refine.run", "refine.status"]) {
handlers[type] = async (payload) => this.handleRefineHostRequest(type, payload);
handlers[type] = createHostRequestHandler(
async (payload, _context) => this.handleRefineHostRequest(type, payload),
contextAwareHostRequestHandler,
);
}
}
if (this._rlmHeartbeatController) {
Expand All @@ -8794,7 +8811,10 @@ export class AgentSession {
"rlm_heartbeat.update",
"rlm_heartbeat.delete",
]) {
handlers[type] = async (payload) => this.handleRlmHeartbeatHostRequest(type, payload);
handlers[type] = createHostRequestHandler(
async (payload, _context) => this.handleRlmHeartbeatHostRequest(type, payload),
contextAwareHostRequestHandler,
);
}
}
const visibleKernelSkillNames = new Set(
Expand Down
Loading