Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/notice-delivery-stage4-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-bundle/runtime": patch
---

Add the #99 stage-4 delivery substrate to the notice ledger: recipient-scoped explicit `acknowledge()` (new `acknowledged` state, the strongest evidenced outcome), optional `retryBudget`/`nextAttemptAt` publish fields with re-attempt semantics evaluated only on admitted events (never an implied timer), a `signalAvailability()` ledger verb recording wire-level `resources/updated` signals as availability receipts (never delivery), and the pure delivery-route selector over per-host advertisements with a typed unavailable outcome. `delivered` remains deliberately absent from the state union because no pinned host supplies cross-actor delivery evidence (2026-09-02 survey on #99); pre-existing durable notices replay unchanged because the new fields are optional and never materialized by parse.
5 changes: 5 additions & 0 deletions .changeset/record-deferred-native-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": patch
---

Record dated deferral rows for every explicitly deferred native host callback from the #258 v2 tracker in a new `deferredNativeEvents` capability-table section: Claude host/UI/protocol callbacks (Setup, UserPromptExpansion, PostToolBatch, MessageDisplay, InstructionsLoaded, CwdChanged, DirectoryAdded, Pre/PostModelSwitch, Elicitation and ElicitationResult, WorktreeCreate/Remove, Notification which belongs to #99 delivery evidence, and the policy_settings exception to config/change blocking), Cursor tab/thought callbacks and the tool-selector native variants, and Codex Interrupt. A pin test enforces the list and the dated reasons.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,68 @@
"source": "https://code.claude.com/docs/en/discover-plugins",
"state": "supported"
},
"deferredNativeEvents": {
"ConfigChange-policy_settings": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: ConfigChange block decisions cannot stop policy_settings changes; recorded so the config/change family's deny channel is not overstated.",
"state": "unavailable"
},
"CwdChanged": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: host environment callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"DirectoryAdded": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: host workspace-registration callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"Elicitation": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: MCP protocol callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"ElicitationResult": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: MCP protocol callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"InstructionsLoaded": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: host context-load callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"MessageDisplay": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: UI display callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"Notification": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: delivery-channel surface tracked by #99 delivery evidence, not an application event route (#258 defer list).",
"state": "unavailable"
},
"PostModelSwitch": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: model-lifecycle callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"PostToolBatch": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: batch-scheduling callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"PreModelSwitch": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: model-lifecycle callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"Setup": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: one-time init/maintenance callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"UserPromptExpansion": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: command-expansion callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"WorktreeCreate": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: worktree-provisioning callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
},
"WorktreeRemove": {
"reason": "retrieved 2026-09-02 from https://code.claude.com/docs/en/hooks: worktree-provisioning callback outside the canonical application-event vocabulary (#258 defer list).",
"state": "unavailable"
}
},
"hooks": {
"config": "hooks/hooks.json",
"events": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
"source": "https://developers.openai.com/codex/cli/reference",
"state": "supported"
},
"deferredNativeEvents": {
"Interrupt": {
"reason": "retrieved 2026-09-02: generated Interrupt schema exists in the codex repository, but release documentation and a pinned CLI revision do not yet agree on its contract (#258 defer list).",
"state": "unavailable"
}
},
"hooks": {
"config": "hooks/hooks.json",
"events": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,52 @@
"source": "https://cursor.com/docs/plugins",
"state": "supported"
},
"deferredNativeEvents": {
"afterAgentResponse": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: native/deferred until a canonical consumer is accepted (#258 defer list).",
"state": "unavailable"
},
"afterAgentThought": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: native/deferred until a canonical consumer is accepted (#258 defer list).",
"state": "unavailable"
},
"afterFileEdit": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: agent-edit variant overlapping canonical tool/after per the #258 selector rule; a dedicated family requires fixture evidence that the generic tool route loses information.",
"state": "unavailable"
},
"afterMCPExecution": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: native variant of canonical tool/after with mcp selector per the #258 selector rule.",
"state": "unavailable"
},
"afterShellExecution": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: native variant of canonical tool/after with shell selector per the #258 selector rule.",
"state": "unavailable"
},
"afterTabFileEdit": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: Tab-surface callback, native/deferred until a canonical consumer is accepted (#258 defer list).",
"state": "unavailable"
},
"beforeMCPExecution": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: native variant of canonical tool/before with mcp selector per the #258 selector rule.",
"state": "unavailable"
},
"beforeReadFile": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: native variant of canonical tool/before with file.read selector per the #258 selector rule.",
"state": "unavailable"
},
"beforeShellExecution": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: native variant of canonical tool/before with shell selector per the #258 selector rule.",
"state": "unavailable"
},
"beforeSubmitPrompt-cloud": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: cloud/desktop availability differences are capability dimensions; cloud rows stay scoped per surface rather than universal-support claims (#258 defer list).",
"state": "unavailable"
},
"beforeTabFileRead": {
"reason": "retrieved 2026-09-02 from https://cursor.com/docs/hooks: Tab-surface callback, native/deferred until a canonical consumer is accepted (#258 defer list).",
"state": "unavailable"
}
},
"hooks": {
"config": "hooks/hooks.json",
"events": {
Expand Down
30 changes: 30 additions & 0 deletions packages/agent-bundle/tests/adapter-capability-states.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,3 +1022,33 @@ it('reports evidence-backed installation support only for real host targets', ()
expect(registry.supports(target, 'install')).toBe(false);
}
});

it('pins dated deferral rows for every explicitly deferred native callback from #258', async () => {
const { readFile } = await import('node:fs/promises');
const tables = {
claude: JSON.parse(await readFile(new URL('../src/adapters/capabilities/claude-2.1.250.json', import.meta.url), 'utf8')) as Record<string, unknown>,
codex: JSON.parse(await readFile(new URL('../src/adapters/capabilities/codex-0.147.0.json', import.meta.url), 'utf8')) as Record<string, unknown>,
cursor: JSON.parse(await readFile(new URL('../src/adapters/capabilities/cursor-2026-08-28.json', import.meta.url), 'utf8')) as Record<string, unknown>,
};
const expected = {
claude: [
'ConfigChange-policy_settings', 'CwdChanged', 'DirectoryAdded', 'Elicitation', 'ElicitationResult',
'InstructionsLoaded', 'MessageDisplay', 'Notification', 'PostModelSwitch', 'PostToolBatch',
'PreModelSwitch', 'Setup', 'UserPromptExpansion', 'WorktreeCreate', 'WorktreeRemove',
],
codex: ['Interrupt'],
cursor: [
'afterAgentResponse', 'afterAgentThought', 'afterFileEdit', 'afterMCPExecution', 'afterShellExecution',
'afterTabFileEdit', 'beforeMCPExecution', 'beforeReadFile', 'beforeShellExecution',
'beforeSubmitPrompt-cloud', 'beforeTabFileRead',
],
} as const;
for (const [host, names] of Object.entries(expected)) {
const deferred = tables[host as keyof typeof tables].deferredNativeEvents as Record<string, { reason: string; state: string }>;
expect(Object.keys(deferred).sort()).toEqual([...names].sort());
for (const name of names) {
expect(deferred[name]!.state).toMatch(/^(unavailable|prohibited)$/u);
expect(deferred[name]!.reason).toMatch(/2026-09-02/u);
}
}
});
4 changes: 2 additions & 2 deletions packages/agent-bundle/tests/adapter-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,10 @@ it('records observed capability versions and rehashes schema snapshots against p
}

if (target === 'claude') {
expect(sha256Hex(capability)).toBe('59e190a26734a6bcca0f22ceee6151ebdd73c690eddb979a78b6b25f0136cb45');
expect(sha256Hex(capability)).toBe('98938d9685411227acee422f90d2e5bb51f33da96ba499aa2ad7b5d3422b37f1');
}
if (target === 'cursor') {
expect(sha256Hex(capability)).toBe('52b1fb8776ffe0848e9ee8c35a604052c59fb292cabbfbaf7a482cd03ce3d918');
expect(sha256Hex(capability)).toBe('e36e84f829f1c69562647a4bc26ecfbaea2dbd0559f194ed7f52bbf133688fe5');
const pluginSchema = JSON.parse(await readFile(
new URL('../src/adapters/schemas/cursor/plugin.schema.json', import.meta.url),
'utf8',
Expand Down
2 changes: 2 additions & 0 deletions packages/rsc-runtime/src/mount/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ const failedLedger = (failure: AgentStateError): AgentNoticeLedger => {
openRequest: async () => Object.freeze({
close: () => undefined,
handle: Object.freeze({
acknowledge: reject,
inbox: reject,
publish: reject,
read: reject,
}),
}),
read: reject,
signalAvailability: reject,
withdraw: reject,
});
};
Expand Down
49 changes: 47 additions & 2 deletions packages/rsc-runtime/src/notices/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,18 @@ export const AGENT_NOTICE_STATES = Object.freeze([
'expired',
'unavailable',
'withdrawn',
'acknowledged',
] as const);

/** V1 contains only states the framework can evidence without host claims. */
/**
* V1 contains only states the framework can evidence without host claims.
* Channel evidence that does not transfer ownership (inbox exposure, wire-level
* resource-updated signals) is recorded as receipts on the notice instead of a
* state: `available` and `read` from the #99 taxonomy map onto the
* `availability` and `exposure` receipts, and `delivered` is deliberately
* absent because no pinned host supplies cross-actor delivery evidence
* (2026-09-02 survey on #99).
*/
export type AgentNoticeState = (typeof AGENT_NOTICE_STATES)[number];

export type AgentNoticePriority = 'low' | 'normal' | 'high';
Expand Down Expand Up @@ -50,19 +59,42 @@ export interface AgentNoticeExposure {
readonly lastInvocationId: string;
}

/**
* Wire-level availability evidence: a `notifications/resources/updated` signal
* was sent for the inbox resource on a live session. Protocol write success is
* only availability, never delivery (#99).
*/
export interface AgentNoticeAvailability {
readonly channel: 'mcp-resource-updated';
readonly count: number;
readonly firstAt: string;
readonly lastAt: string;
}

export interface AgentNoticeAcknowledgement {
readonly acknowledgedAt: string;
readonly invocationId: string;
}

export type AgentNoticeUnavailableReason = 'delivery-authorization-unavailable';

export interface AgentNotice {
readonly acknowledgement?: AgentNoticeAcknowledgement;
readonly attempts: readonly AgentNoticeAttemptReceipt[];
readonly availability?: AgentNoticeAvailability;
readonly content: AgentDocumentSnapshot;
readonly createdAt: string;
readonly dedupeKey?: string;
readonly expiredAt?: string;
readonly expiresAt?: string;
readonly exposure?: AgentNoticeExposure;
readonly id: string;
/** Admissions before this instant leave the notice pending (V1: evaluated only on admitted events, never by an implied timer). */
readonly nextAttemptAt?: string;
readonly priority: AgentNoticePriority;
readonly recipient: AgentRecipient;
/** Maximum next-event attempt receipts before admission stops re-attempting; absent means 1. */
readonly retryBudget?: number;
readonly state: AgentNoticeState;
readonly unavailableAt?: string;
readonly unavailableReason?: AgentNoticeUnavailableReason;
Expand All @@ -78,8 +110,11 @@ export interface AgentNoticePublishInput {
readonly content: AgentDocumentSnapshot;
readonly dedupeKey?: string;
readonly expiresAt?: string;
readonly nextAttemptAt?: string;
readonly priority: AgentNoticePriority;
readonly recipient: AgentRecipient;
/** Defaults to 1 (single next-event attempt). */
readonly retryBudget?: number;
}

export interface AgentNoticePublishOptions {
Expand Down Expand Up @@ -109,7 +144,7 @@ export type AgentNoticeAuthorizationDecision =

export interface AgentNoticeAuthorizationRequest {
readonly noticeId?: string;
readonly phase: 'deliver' | 'publish' | 'read';
readonly phase: 'acknowledge' | 'deliver' | 'publish' | 'read';
readonly principal: AgentNoticePrincipal;
readonly recipient: AgentRecipient;
}
Expand All @@ -124,6 +159,8 @@ export interface AgentNoticeDelivery {
}

export interface AgentNoticesHandle {
/** Recipient-scoped explicit acknowledgement; the strongest evidenced state. */
acknowledge(id: string): Promise<AgentNotice>;
Comment thread
ScriptedAlchemy marked this conversation as resolved.
inbox(): Promise<readonly AgentNotice[]>;
publish(input: AgentNoticePublishInput, options: AgentNoticePublishOptions): Promise<AgentNoticePublishResult>;
read(): Promise<readonly AgentNoticeDelivery[]>;
Expand All @@ -140,10 +177,18 @@ export interface AgentNoticeRequestLease {
close(): void;
}

export interface AgentNoticeAvailabilitySignalOptions {
readonly at: string;
readonly idempotencyKey: string;
readonly noticeIds: readonly string[];
}

export interface AgentNoticeLedger {
expire(options: AgentNoticeExpiryOptions): Promise<AgentNoticeLedgerSnapshot>;
openRequest(request: AgentNoticeRequest): Promise<AgentNoticeRequestLease>;
read(): Promise<AgentNoticeLedgerSnapshot>;
/** Records a wire-level resources/updated signal; availability, never delivery. */
signalAvailability(options: AgentNoticeAvailabilitySignalOptions): Promise<AgentNoticeLedgerSnapshot>;
withdraw(id: string, options: AgentNoticeWithdrawOptions): Promise<AgentNoticeLedgerSnapshot>;
}

Expand Down
13 changes: 13 additions & 0 deletions packages/rsc-runtime/src/notices/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ export {
} from './contract.js';
export type {
AgentNotice,
AgentNoticeAcknowledgement,
AgentNoticeAttemptReceipt,
AgentNoticeAuthorizationDecision,
AgentNoticeAuthorizationRequest,
AgentNoticeAuthorizer,
AgentNoticeAvailability,
AgentNoticeAvailabilitySignalOptions,
AgentNoticeDelivery,
AgentNoticeErrorCode,
AgentNoticeExposure,
Expand All @@ -35,6 +38,16 @@ export type {
AgentNoticeWithdrawOptions,
AgentRecipient,
} from './contract.js';
export {
AGENT_NOTICE_DELIVERY_ROUTES,
selectNoticeDeliveryRoutes,
} from './router.js';
export type {
AgentNoticeDeliveryAdvertisement,
AgentNoticeDeliveryRoute,
AgentNoticeDeliveryRouteState,
AgentNoticeRouteSelection,
} from './router.js';
export {
createAgentNoticeLedger,
} from './ledger.js';
Expand Down
Loading
Loading