@velanir/openclaw-pending-context 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @velanir/openclaw-pending-context
2
+
3
+ `@velanir/openclaw-pending-context` makes an interactive scheduling reply safe
4
+ to interpret without treating an old transcript mirror as workflow state.
5
+
6
+ ## Canonical 0.4.0 behavior
7
+
8
+ 1. It reads pending state only from the responsibility runner and injects it
9
+ only when the exact agent, channel, session/conversation, account, and
10
+ authenticated user match. Strict user and session scope both default on.
11
+ 2. It registers `responsibility_pending` first, then
12
+ `responsibility_select_option`, `responsibility_reject`,
13
+ `responsibility_change`, and `responsibility_status`. A model never passes a
14
+ `userMessageId`; a host-observed inbound message is bound internally through
15
+ the manifest-declared trusted tool policy.
16
+ 3. It quiets only a fresh inbound run that has matching pending context. With
17
+ `suppressNonFinalReplies=true` (the default), tool/progress reply payloads
18
+ for that run are cancelled. Ordinary chat and stale/unscoped sessions are
19
+ not put into global final-only mode.
20
+ 4. The first admitted action ends the run's tool budget. Later tool calls are
21
+ blocked. The final reply and the persisted assistant message are rewritten
22
+ from the authoritative runner receipt; a model claim that a meeting was
23
+ scheduled without an action is replaced with a truthful failure.
24
+ 5. A scheduling-success final requires a non-dry-run `select_option` receipt
25
+ with an Outlook event id, Zoom join URL, and counterpart-reply receipt.
26
+ Partial results, errors, and dry runs never become success claims.
27
+
28
+ The package deliberately chose the stronger Riley/Mae common behavior as the
29
+ source of truth. It does not copy live compiled bytes or make ordinary Teams
30
+ chat quiet merely because a responsibility happens to have pending work.
31
+
32
+ ## Rollout and rollback
33
+
34
+ `dryRun` defaults to `true`. Set it to `false` only for a reviewed canary.
35
+ That alone still cannot execute an effect: the source must declare an explicit
36
+ `actionCommand`; there is no default effect argv. Roll back immediately by
37
+ setting `dryRun` to `true`, or set `suppressNonFinalReplies` to `false` for
38
+ diagnosis while preserving the receipt/final guard.
39
+
40
+ The plugin requires an OpenClaw runtime compatible with `2026.7.1-beta.5` or
41
+ newer and declares its trusted policy in `openclaw.plugin.json`. Platform
42
+ rendering/install work owns canary selection and the exact reviewed action
43
+ bridge; this package does not deploy itself or modify runner business logic.
44
+
45
+ ## Develop
46
+
47
+ ```bash
48
+ pnpm --filter=@velanir/openclaw-pending-context test
49
+ pnpm --filter=@velanir/openclaw-pending-context check-types
50
+ pnpm --filter=@velanir/openclaw-pending-context build
51
+ ```
52
+
53
+ `DESIGN.md` documents the original phase-one pending-state contract. This README
54
+ and the source/tests are the canonical 0.4.0 finalization contract.
@@ -0,0 +1,246 @@
1
+ type PluginLogger = {
2
+ debug?: (message: string) => void;
3
+ info?: (message: string) => void;
4
+ warn?: (message: string) => void;
5
+ error?: (message: string) => void;
6
+ };
7
+ type AgentToolLike = {
8
+ name: string;
9
+ label: string;
10
+ description: string;
11
+ parameters: unknown;
12
+ execute: (toolCallId: string, params: unknown, signal?: unknown, onUpdate?: unknown) => Promise<AgentToolResultLike>;
13
+ };
14
+ type AgentToolResultLike = {
15
+ content: Array<{
16
+ type: "text";
17
+ text: string;
18
+ }>;
19
+ details: unknown;
20
+ };
21
+ type ToolFactoryContext = {
22
+ agentId?: string;
23
+ sessionKey?: string;
24
+ messageChannel?: string;
25
+ agentAccountId?: string;
26
+ requesterSenderId?: string;
27
+ requesterMessageId?: string;
28
+ deliveryContext?: {
29
+ channel?: string;
30
+ accountId?: string;
31
+ to?: string;
32
+ threadId?: string | number;
33
+ };
34
+ };
35
+ type AgentToolFactory = (ctx: ToolFactoryContext) => AgentToolLike | AgentToolLike[] | null | undefined;
36
+ type AgentTurnPrepareContext = {
37
+ runId?: string;
38
+ agentId?: string;
39
+ sessionKey?: string;
40
+ channel?: string;
41
+ channelId?: string;
42
+ chatId?: string;
43
+ senderId?: string;
44
+ messageProvider?: string;
45
+ workspaceDir?: string;
46
+ };
47
+ type PluginJsonValue = null | boolean | number | string | PluginJsonValue[] | {
48
+ [key: string]: PluginJsonValue;
49
+ };
50
+ type SessionExtensionRegistration = {
51
+ namespace: string;
52
+ description: string;
53
+ project?: (ctx: {
54
+ sessionKey: string;
55
+ sessionId?: string;
56
+ state: PluginJsonValue | undefined;
57
+ }) => PluginJsonValue | undefined;
58
+ };
59
+ type TrustedToolPolicyEvent = {
60
+ toolName: string;
61
+ params: Record<string, unknown>;
62
+ runId?: string;
63
+ toolCallId?: string;
64
+ };
65
+ type TrustedToolPolicyContext = {
66
+ agentId?: string;
67
+ sessionKey?: string;
68
+ sessionId?: string;
69
+ runId?: string;
70
+ toolName: string;
71
+ toolCallId?: string;
72
+ channelId?: string;
73
+ getSessionExtension?: (namespace: string) => PluginJsonValue | undefined;
74
+ };
75
+ type TrustedToolPolicyDecision = {
76
+ params?: Record<string, unknown>;
77
+ block?: boolean;
78
+ blockReason?: string;
79
+ };
80
+ type TrustedToolPolicyRegistration = {
81
+ id: string;
82
+ description: string;
83
+ evaluate: (event: TrustedToolPolicyEvent, ctx: TrustedToolPolicyContext) => TrustedToolPolicyDecision | void | Promise<TrustedToolPolicyDecision | void>;
84
+ };
85
+ type RunContextPatch = {
86
+ runId: string;
87
+ namespace: string;
88
+ value?: PluginJsonValue;
89
+ unset?: boolean;
90
+ };
91
+ type RunContextQuery = {
92
+ runId: string;
93
+ namespace: string;
94
+ };
95
+ type OpenClawPluginApi = {
96
+ pluginConfig?: Record<string, unknown>;
97
+ logger?: PluginLogger;
98
+ setRunContext?: (patch: RunContextPatch) => boolean;
99
+ getRunContext?: (params: RunContextQuery) => PluginJsonValue | undefined;
100
+ runContext?: {
101
+ setRunContext: (patch: RunContextPatch) => boolean;
102
+ getRunContext: (params: RunContextQuery) => PluginJsonValue | undefined;
103
+ };
104
+ registerTool: (tool: AgentToolLike | AgentToolFactory, opts?: {
105
+ name?: string;
106
+ names?: string[];
107
+ optional?: boolean;
108
+ }) => void;
109
+ registerSessionExtension: (extension: SessionExtensionRegistration) => void;
110
+ registerTrustedToolPolicy: (policy: TrustedToolPolicyRegistration) => void;
111
+ on: (hookName: string, handler: (event: unknown, ctx: unknown) => unknown | Promise<unknown>, opts?: {
112
+ priority?: number;
113
+ timeoutMs?: number;
114
+ }) => void;
115
+ };
116
+
117
+ type TurnScope = {
118
+ agentId?: string;
119
+ channel?: string;
120
+ userId?: string;
121
+ sessionKey?: string;
122
+ accountId?: string;
123
+ conversationId?: string;
124
+ };
125
+
126
+ type TrustedInboundAuthorization = {
127
+ messageId: string;
128
+ senderId: string;
129
+ sessionKey: string;
130
+ runId?: string;
131
+ channel?: string;
132
+ accountId?: string;
133
+ conversationId?: string;
134
+ receivedAt: number;
135
+ };
136
+ type InboundHookEvent = {
137
+ messageId?: string;
138
+ senderId?: string;
139
+ from?: string;
140
+ sessionKey?: string;
141
+ runId?: string;
142
+ channel?: string;
143
+ };
144
+ type InboundHookContext = {
145
+ messageId?: string;
146
+ senderId?: string;
147
+ sessionKey?: string;
148
+ runId?: string;
149
+ channelId?: string;
150
+ accountId?: string;
151
+ conversationId?: string;
152
+ };
153
+ type ToolBindEvent = {
154
+ toolName?: string;
155
+ toolCallId?: string;
156
+ runId?: string;
157
+ };
158
+ type ToolBindContext = {
159
+ toolName?: string;
160
+ toolCallId?: string;
161
+ runId?: string;
162
+ sessionKey?: string;
163
+ };
164
+ type AuthorizationConsumeError = "missing_binding" | "message_replayed" | "session_mismatch" | "user_mismatch";
165
+ type AuthorizationConsumeResult = {
166
+ authorization: TrustedInboundAuthorization | null;
167
+ error?: AuthorizationConsumeError;
168
+ };
169
+ type InboundAuthorizationStore = {
170
+ recordTrusted(record: TrustedInboundAuthorization): boolean;
171
+ recordInbound(event: InboundHookEvent, ctx: InboundHookContext): boolean;
172
+ bindTool(event: ToolBindEvent, ctx: ToolBindContext): boolean;
173
+ hasFreshBinding(scope: TurnScope, runId?: string): boolean;
174
+ consumeDetailed(toolCallId: string, scope: TurnScope): AuthorizationConsumeResult;
175
+ consume(toolCallId: string, scope: TurnScope): TrustedInboundAuthorization | null;
176
+ };
177
+
178
+ type DeliveryHookEvent = {
179
+ runId?: string;
180
+ toolName?: string;
181
+ toolCallId?: string;
182
+ result?: unknown;
183
+ error?: unknown;
184
+ kind?: string;
185
+ };
186
+ type DeliveryHookContext = {
187
+ runId?: string;
188
+ toolName?: string;
189
+ toolCallId?: string;
190
+ sessionKey?: string;
191
+ };
192
+ type TransactionDeliveryStore = {
193
+ markPendingTurn(runIdValue: unknown, sessionKeyValue: unknown): boolean;
194
+ beginAction(event: DeliveryHookEvent, ctx: DeliveryHookContext): {
195
+ tracked: boolean;
196
+ duplicate: boolean;
197
+ };
198
+ failAction(event: DeliveryHookEvent, ctx: DeliveryHookContext, errorCode: string | undefined): boolean;
199
+ finishAction(event: DeliveryHookEvent, ctx: DeliveryHookContext): boolean;
200
+ blockFurtherTool(event: DeliveryHookEvent, ctx: DeliveryHookContext): boolean;
201
+ suppressNonFinal(event: DeliveryHookEvent): boolean;
202
+ expectedFinalText(runIdValue: unknown): string | undefined;
203
+ expectedFinalTextForToolCall(toolCallIdValue: unknown): string | undefined;
204
+ pendingWithoutAction(runIdValue: unknown): boolean;
205
+ claimNoActionFailureForSession(sessionKeyValue: unknown): string | undefined;
206
+ authoritativeFinalForSession(sessionKeyValue: unknown, modelClaimsSuccess: boolean): string | undefined;
207
+ markFinalDelivered(sessionKeyValue: unknown): boolean;
208
+ finalAlreadyDelivered(sessionKeyValue: unknown): boolean;
209
+ claimFinalTextForSession(sessionKeyValue: unknown): string | undefined;
210
+ finalizeReply(event: DeliveryHookEvent): {
211
+ text?: string;
212
+ } | undefined;
213
+ };
214
+
215
+ type RunnerExecInput = {
216
+ command: string[];
217
+ cwd: string;
218
+ timeoutMs: number;
219
+ maxOutputBytes: number;
220
+ stdin?: string;
221
+ };
222
+ type RunnerExecResult = {
223
+ ok: boolean;
224
+ stdout?: string;
225
+ error?: string;
226
+ truncated?: boolean;
227
+ };
228
+ type RunnerExec = (input: RunnerExecInput) => RunnerExecResult;
229
+
230
+ declare const PLUGIN_ID = "velanir-pending-context";
231
+ declare const TRUSTED_ACTION_POLICY_ID = "authoritative-pending-action";
232
+ type PendingContextRuntime = {
233
+ authorization?: InboundAuthorizationStore;
234
+ delivery?: TransactionDeliveryStore;
235
+ };
236
+ declare function turnScopeFromContext(ctx: AgentTurnPrepareContext): TurnScope;
237
+ declare function toolScopeFromContext(ctx: ToolFactoryContext): TurnScope;
238
+ declare function registerPendingContextPlugin(api: OpenClawPluginApi, exec: RunnerExec, runtime?: PendingContextRuntime): void;
239
+ declare const _default: {
240
+ id: string;
241
+ name: string;
242
+ description: string;
243
+ register(api: OpenClawPluginApi): void;
244
+ };
245
+
246
+ export { PLUGIN_ID, type PendingContextRuntime, TRUSTED_ACTION_POLICY_ID, _default as default, registerPendingContextPlugin, toolScopeFromContext, turnScopeFromContext };