@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/dist/index.js ADDED
@@ -0,0 +1,1540 @@
1
+ // src/api.ts
2
+ import { definePluginEntry as defineOpenClawPluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ function definePluginEntry(entry) {
4
+ return defineOpenClawPluginEntry(entry);
5
+ }
6
+
7
+ // src/authorization.ts
8
+ var ACTION_TOOLS = /* @__PURE__ */ new Set([
9
+ "responsibility_select_option",
10
+ "responsibility_reject",
11
+ "responsibility_change"
12
+ ]);
13
+ function clean(value) {
14
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
15
+ }
16
+ function cleanIdentity(value) {
17
+ const cleaned = clean(value);
18
+ if (!cleaned) return void 0;
19
+ let normalized = cleaned;
20
+ for (; ; ) {
21
+ const next = normalized.replace(/^(?:user|chat|channel|msteams):/i, "");
22
+ if (next === normalized) return normalized.toLowerCase();
23
+ normalized = next;
24
+ }
25
+ }
26
+ function trustedInboundFromHook(event, ctx, receivedAt = Date.now()) {
27
+ const messageId = clean(event.messageId) ?? clean(ctx.messageId);
28
+ const senderId = cleanIdentity(event.senderId) ?? cleanIdentity(ctx.senderId) ?? cleanIdentity(event.from);
29
+ const sessionKey = clean(event.sessionKey) ?? clean(ctx.sessionKey);
30
+ const runId = clean(event.runId) ?? clean(ctx.runId);
31
+ if (!messageId || !senderId || !sessionKey) return null;
32
+ return {
33
+ messageId,
34
+ senderId,
35
+ sessionKey,
36
+ ...runId ? { runId } : {},
37
+ ...clean(event.channel) ?? clean(ctx.channelId) ? { channel: clean(event.channel) ?? clean(ctx.channelId) } : {},
38
+ ...clean(ctx.accountId) ? { accountId: clean(ctx.accountId) } : {},
39
+ ...clean(ctx.conversationId) ? { conversationId: clean(ctx.conversationId) } : {},
40
+ receivedAt
41
+ };
42
+ }
43
+ function createInboundAuthorizationState() {
44
+ return {
45
+ byRun: /* @__PURE__ */ new Map(),
46
+ bySession: /* @__PURE__ */ new Map(),
47
+ byToolCall: /* @__PURE__ */ new Map(),
48
+ usedMessageIds: /* @__PURE__ */ new Map()
49
+ };
50
+ }
51
+ function sharedInboundAuthorizationState() {
52
+ const runtime = globalThis;
53
+ runtime.__velanirPendingContextAuthorizationV1 ??= createInboundAuthorizationState();
54
+ return runtime.__velanirPendingContextAuthorizationV1;
55
+ }
56
+ function createInboundAuthorizationStore(ttlMs, now = Date.now, state = createInboundAuthorizationState()) {
57
+ const { byRun, bySession, byToolCall, usedMessageIds } = state;
58
+ const fresh = (record) => record && now() - record.receivedAt <= ttlMs ? record : void 0;
59
+ const prune = () => {
60
+ for (const [key, record] of byRun) if (!fresh(record)) byRun.delete(key);
61
+ for (const [key, record] of bySession) if (!fresh(record)) bySession.delete(key);
62
+ for (const [key, record] of byToolCall) if (!fresh(record)) byToolCall.delete(key);
63
+ for (const [key, usedAt] of usedMessageIds) if (now() - usedAt > ttlMs) usedMessageIds.delete(key);
64
+ };
65
+ return {
66
+ recordTrusted(record) {
67
+ prune();
68
+ if (!fresh(record)) return false;
69
+ if (record.runId) byRun.set(record.runId, record);
70
+ bySession.set(record.sessionKey, record);
71
+ return true;
72
+ },
73
+ recordInbound(event, ctx) {
74
+ prune();
75
+ const record = trustedInboundFromHook(event, ctx, now());
76
+ return record ? this.recordTrusted(record) : false;
77
+ },
78
+ bindTool(event, ctx) {
79
+ prune();
80
+ const toolName = clean(event.toolName) ?? clean(ctx.toolName);
81
+ const toolCallId = clean(event.toolCallId) ?? clean(ctx.toolCallId);
82
+ if (!toolName || !ACTION_TOOLS.has(toolName) || !toolCallId) return false;
83
+ const runId = clean(event.runId) ?? clean(ctx.runId);
84
+ const sessionKey = clean(ctx.sessionKey);
85
+ const record = fresh(runId ? byRun.get(runId) : void 0) ?? fresh(sessionKey ? bySession.get(sessionKey) : void 0);
86
+ if (!record || sessionKey && record.sessionKey !== sessionKey) return false;
87
+ byToolCall.set(toolCallId, record);
88
+ return true;
89
+ },
90
+ hasFreshBinding(scope, runId) {
91
+ prune();
92
+ const record = fresh(runId ? byRun.get(runId) : void 0) ?? fresh(scope.sessionKey ? bySession.get(scope.sessionKey) : void 0);
93
+ if (!record || !scope.sessionKey || record.sessionKey !== scope.sessionKey) return false;
94
+ if (!scope.userId || cleanIdentity(record.senderId) !== cleanIdentity(scope.userId)) return false;
95
+ return true;
96
+ },
97
+ consumeDetailed(toolCallId, scope) {
98
+ prune();
99
+ const record = fresh(byToolCall.get(toolCallId));
100
+ byToolCall.delete(toolCallId);
101
+ if (!record) {
102
+ return { authorization: null, error: "missing_binding" };
103
+ }
104
+ if (usedMessageIds.has(record.messageId)) {
105
+ return { authorization: null, error: "message_replayed" };
106
+ }
107
+ if (!scope.sessionKey || record.sessionKey !== scope.sessionKey) {
108
+ return { authorization: null, error: "session_mismatch" };
109
+ }
110
+ if (!scope.userId || cleanIdentity(record.senderId) !== cleanIdentity(scope.userId)) {
111
+ return { authorization: null, error: "user_mismatch" };
112
+ }
113
+ usedMessageIds.set(record.messageId, now());
114
+ return { authorization: record };
115
+ },
116
+ consume(toolCallId, scope) {
117
+ return this.consumeDetailed(toolCallId, scope).authorization;
118
+ }
119
+ };
120
+ }
121
+
122
+ // src/config.ts
123
+ var DEFAULT_TIMEOUT_MS = 5e3;
124
+ var DEFAULT_MAX_OUTPUT_BYTES = 262144;
125
+ var DEFAULT_MAX_INJECTION_CHARS = 1800;
126
+ var DEFAULT_MAX_ITEMS = 6;
127
+ var DEFAULT_COMMAND = ["node", "runner.mjs", "pending"];
128
+ var DEFAULT_ACTION_TIMEOUT_MS = 5e4;
129
+ var DEFAULT_AUTHORIZATION_TTL_MS = 2 * 6e4;
130
+ function isRecord(value) {
131
+ return typeof value === "object" && value !== null && !Array.isArray(value);
132
+ }
133
+ function readString(value) {
134
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
135
+ }
136
+ function readPositiveInteger(value, fallback) {
137
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
138
+ }
139
+ function readBoolean(value, fallback) {
140
+ return typeof value === "boolean" ? value : fallback;
141
+ }
142
+ function normalizeCommand(value) {
143
+ if (!Array.isArray(value)) {
144
+ return [...DEFAULT_COMMAND];
145
+ }
146
+ const parts = value.map(readString).filter((entry) => Boolean(entry));
147
+ return parts.length > 0 ? parts : [...DEFAULT_COMMAND];
148
+ }
149
+ function normalizeActionCommand(value) {
150
+ if (!Array.isArray(value)) {
151
+ return void 0;
152
+ }
153
+ const parts = value.map(readString).filter((entry) => Boolean(entry));
154
+ return parts.length > 0 ? parts : void 0;
155
+ }
156
+ function normalizeSource(value, indexHint) {
157
+ if (!isRecord(value)) {
158
+ return null;
159
+ }
160
+ const workspacePath = readString(value.workspacePath);
161
+ const responsibilityId = readString(value.responsibilityId);
162
+ if (!workspacePath || !responsibilityId) {
163
+ return null;
164
+ }
165
+ const id = readString(value.id) ?? `${responsibilityId}#${indexHint}`;
166
+ const actionCommand = normalizeActionCommand(value.actionCommand);
167
+ return {
168
+ id,
169
+ responsibilityId,
170
+ ...readString(value.agentId) ? { agentId: readString(value.agentId) } : {},
171
+ workspacePath,
172
+ command: normalizeCommand(value.command),
173
+ ...actionCommand ? { actionCommand } : {},
174
+ timeoutMs: readPositiveInteger(value.timeoutMs, DEFAULT_TIMEOUT_MS),
175
+ ...actionCommand ? { actionTimeoutMs: readPositiveInteger(value.actionTimeoutMs, DEFAULT_ACTION_TIMEOUT_MS) } : {},
176
+ maxOutputBytes: readPositiveInteger(value.maxOutputBytes, DEFAULT_MAX_OUTPUT_BYTES),
177
+ ...readString(value.channel) ? { channel: readString(value.channel) } : {},
178
+ ...readString(value.userId) ? { userId: readString(value.userId) } : {},
179
+ ...readString(value.sessionKey) ? { sessionKey: readString(value.sessionKey) } : {},
180
+ ...readString(value.accountId) ? { accountId: readString(value.accountId) } : {},
181
+ ...readString(value.conversationId) ? { conversationId: readString(value.conversationId) } : {}
182
+ };
183
+ }
184
+ function normalizeConfig(pluginConfig) {
185
+ const record = isRecord(pluginConfig) ? pluginConfig : {};
186
+ const loggingRecord = isRecord(record.logging) ? record.logging : {};
187
+ const rawSources = Array.isArray(record.sources) ? record.sources : [];
188
+ const sources = [];
189
+ rawSources.forEach((entry, i) => {
190
+ const normalized = normalizeSource(entry, i);
191
+ if (normalized) {
192
+ sources.push(normalized);
193
+ }
194
+ });
195
+ return {
196
+ dryRun: readBoolean(record.dryRun, true),
197
+ maxInjectionChars: readPositiveInteger(record.maxInjectionChars, DEFAULT_MAX_INJECTION_CHARS),
198
+ maxItemsInInjection: readPositiveInteger(record.maxItemsInInjection, DEFAULT_MAX_ITEMS),
199
+ strictUserScope: readBoolean(record.strictUserScope, true),
200
+ strictSessionScope: readBoolean(record.strictSessionScope, true),
201
+ authorizationTtlMs: readPositiveInteger(record.authorizationTtlMs, DEFAULT_AUTHORIZATION_TTL_MS),
202
+ suppressNonFinalReplies: readBoolean(record.suppressNonFinalReplies, true),
203
+ sources,
204
+ logging: {
205
+ decisions: loggingRecord.decisions !== false,
206
+ includeContent: loggingRecord.includeContent === true
207
+ }
208
+ };
209
+ }
210
+
211
+ // src/delivery.ts
212
+ var DELIVERY_ACTION_TOOLS = /* @__PURE__ */ new Set([
213
+ "responsibility_select_option",
214
+ "responsibility_reject",
215
+ "responsibility_change"
216
+ ]);
217
+ function clean2(value) {
218
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
219
+ }
220
+ function claimsSchedulingSuccess(value) {
221
+ const text = clean2(value)?.toLowerCase();
222
+ if (!text) return false;
223
+ const withoutNegativeClaims = text.replace(
224
+ /\b(?:no|not)\s+(?:calendar\s+)?(?:meeting|event|invite|booking)\b[^.!?\n]{0,80}\b(?:scheduled|booked|created|sent|confirmed)\b/g,
225
+ ""
226
+ ).replace(
227
+ /\b(?:could(?:n't| not)|did(?:n't| not)|was(?:n't| not)|is(?:n't| not)|has(?:n't| not)|haven(?:'t| not))\b[^.!?\n]{0,80}\b(?:schedule|scheduled|book|booked|create|created|send|sent|confirm|confirmed)\b/g,
228
+ ""
229
+ );
230
+ return /\b(?:done|finished|all set)\b/.test(withoutNegativeClaims) || /\b(?:i|we)(?:'ve|\s+have)?\s+(?:scheduled|booked|created|sent)\b/.test(withoutNegativeClaims) || /\b(?:i|we)(?:'ve|\s+have)?\s+confirmed\s+(?:the\s+)?(?:booking|meeting|slot|invite)\b/.test(withoutNegativeClaims) || /\b(?:meeting|event|invite|booking)\s+(?:(?:has been|is|was)\s+)?(?:scheduled|booked|created|sent|confirmed)\b/.test(
231
+ withoutNegativeClaims
232
+ ) || /\b(?:calendar event|meeting invite)\s+(?:was|has been)\s+(?:created|sent)\b/.test(withoutNegativeClaims);
233
+ }
234
+ function asRecord(value) {
235
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
236
+ }
237
+ function parseToolOutcome(result) {
238
+ const resultRecord = asRecord(result);
239
+ if (!resultRecord) return void 0;
240
+ const details = asRecord(resultRecord.details);
241
+ if (details) return details;
242
+ const content = Array.isArray(resultRecord.content) ? resultRecord.content : [];
243
+ for (const item of content) {
244
+ const text = clean2(asRecord(item)?.text);
245
+ if (!text) continue;
246
+ try {
247
+ const parsed = asRecord(JSON.parse(text));
248
+ if (parsed) return parsed;
249
+ } catch {
250
+ }
251
+ }
252
+ return resultRecord;
253
+ }
254
+ function buildTransactionFailureText(errorCode) {
255
+ if (errorCode === "dry_run") {
256
+ return "I checked your selection in dry-run mode. No calendar event, video meeting, or confirmation email was created.";
257
+ }
258
+ if (errorCode === "authorization_unavailable") {
259
+ return "I couldn't complete the scheduling because this action was not securely bound to your latest message. No calendar event was created.";
260
+ }
261
+ if (errorCode === "state_version_mismatch" || errorCode === "interaction_version_conflict") {
262
+ return "I couldn't complete the scheduling because the meeting details changed before the transaction finished. No calendar event was created.";
263
+ }
264
+ if (errorCode === "slot_not_offered" || errorCode === "slot_no_longer_available") {
265
+ return "I couldn't complete the scheduling because that time is no longer one of the available options. No calendar event was created.";
266
+ }
267
+ if (errorCode === "slot_in_past") {
268
+ return "I couldn't complete the scheduling because that meeting time has already passed. No calendar event was created.";
269
+ }
270
+ if (errorCode === "zoom_create_failed") {
271
+ return "I couldn't create the Zoom meeting, so no calendar event or confirmation email was created. I need the Zoom connection fixed before I retry.";
272
+ }
273
+ if (errorCode === "action_not_allowed_for_stage") {
274
+ return "I couldn't confirm that slot because this scheduling request is not currently awaiting your selection. No calendar event or confirmation email was created.";
275
+ }
276
+ if (errorCode === "transaction_recovery_required") {
277
+ return "I couldn't retry this meeting because the previous booking attempt is waiting for recovery. No calendar event or confirmation email was created.";
278
+ }
279
+ if (errorCode === "transaction_receipt_invalid") {
280
+ return "I couldn't verify the complete scheduling receipts, so I won't claim the meeting was completed. Check the calendar and sent mail before retrying.";
281
+ }
282
+ return "I couldn't complete the scheduling transaction. No calendar event was created or changed.";
283
+ }
284
+ function buildTransactionSuccessText(outcome) {
285
+ const explicit = clean2(outcome.userVisibleText);
286
+ if (explicit) return explicit;
287
+ const result = asRecord(outcome.result);
288
+ const counterpart = clean2(result?.counterpart);
289
+ const display = clean2(result?.display) ?? clean2(result?.start);
290
+ const replied = result?.replied === true;
291
+ const subject = counterpart ? ` with ${counterpart}` : "";
292
+ const when = display ? ` for ${display}` : "";
293
+ const receipt = replied ? "The calendar event was created and the confirmation was sent." : "The calendar event was created.";
294
+ return `Done \u2014 I scheduled the meeting${subject}${when}. ${receipt}`;
295
+ }
296
+ function hasCompleteSchedulingReceipts(outcome) {
297
+ if (outcome?.ok !== true || outcome.dryRun !== false || outcome.action !== "select_option" || outcome.status !== "scheduled") {
298
+ return false;
299
+ }
300
+ const result = asRecord(outcome.result);
301
+ return Boolean(clean2(result?.eventId) && clean2(result?.joinUrl) && result?.replied === true);
302
+ }
303
+ function createTransactionDeliveryState() {
304
+ return {
305
+ byRun: /* @__PURE__ */ new Map(),
306
+ runByToolCall: /* @__PURE__ */ new Map()
307
+ };
308
+ }
309
+ function sharedTransactionDeliveryState() {
310
+ const runtime = globalThis;
311
+ runtime.__velanirPendingContextDeliveryV1 ??= createTransactionDeliveryState();
312
+ return runtime.__velanirPendingContextDeliveryV1;
313
+ }
314
+ function createTransactionDeliveryStore(ttlMs, now = Date.now, state = createTransactionDeliveryState()) {
315
+ const { byRun, runByToolCall } = state;
316
+ const prune = () => {
317
+ for (const [runId, record] of byRun) {
318
+ if (now() - record.updatedAt > ttlMs) byRun.delete(runId);
319
+ }
320
+ for (const [toolCallId, runId] of runByToolCall) {
321
+ if (!byRun.has(runId)) runByToolCall.delete(toolCallId);
322
+ }
323
+ };
324
+ const resolveRun = (event, ctx) => clean2(event.runId) ?? clean2(ctx.runId);
325
+ return {
326
+ markPendingTurn(runIdValue, sessionKeyValue) {
327
+ prune();
328
+ const runId = clean2(runIdValue);
329
+ if (!runId) return false;
330
+ const existing = byRun.get(runId);
331
+ byRun.set(runId, {
332
+ runId,
333
+ ...clean2(sessionKeyValue) ? { sessionKey: clean2(sessionKeyValue) } : {},
334
+ updatedAt: now(),
335
+ actionAttempted: existing?.actionAttempted ?? false,
336
+ actionFinished: existing?.actionFinished ?? false,
337
+ actionSucceeded: existing?.actionSucceeded ?? false,
338
+ ...existing?.finalTextClaimed ? { finalTextClaimed: true } : {},
339
+ ...existing?.userVisibleText ? { userVisibleText: existing.userVisibleText } : {},
340
+ ...existing?.errorCode ? { errorCode: existing.errorCode } : {},
341
+ ...existing?.finalDelivered ? { finalDelivered: true } : {}
342
+ });
343
+ return true;
344
+ },
345
+ beginAction(event, ctx) {
346
+ prune();
347
+ const toolName = clean2(event.toolName) ?? clean2(ctx.toolName);
348
+ if (!toolName || !DELIVERY_ACTION_TOOLS.has(toolName)) {
349
+ return { tracked: false, duplicate: false };
350
+ }
351
+ const runId = resolveRun(event, ctx);
352
+ const toolCallId = clean2(event.toolCallId) ?? clean2(ctx.toolCallId);
353
+ if (!runId || !toolCallId) return { tracked: false, duplicate: false };
354
+ const existing = byRun.get(runId);
355
+ if (existing?.actionAttempted) {
356
+ return { tracked: true, duplicate: true };
357
+ }
358
+ byRun.set(runId, {
359
+ runId,
360
+ ...clean2(ctx.sessionKey) ? { sessionKey: clean2(ctx.sessionKey) } : {},
361
+ updatedAt: now(),
362
+ actionAttempted: true,
363
+ actionFinished: false,
364
+ actionSucceeded: false
365
+ });
366
+ runByToolCall.set(toolCallId, runId);
367
+ return { tracked: true, duplicate: false };
368
+ },
369
+ failAction(event, ctx, errorCode) {
370
+ prune();
371
+ const toolCallId = clean2(event.toolCallId) ?? clean2(ctx.toolCallId);
372
+ const runId = (toolCallId ? runByToolCall.get(toolCallId) : void 0) ?? resolveRun(event, ctx);
373
+ if (!runId) return false;
374
+ const record = byRun.get(runId);
375
+ if (!record) return false;
376
+ record.updatedAt = now();
377
+ record.actionFinished = true;
378
+ record.actionSucceeded = false;
379
+ record.errorCode = clean2(errorCode) ?? "transaction_failed";
380
+ return true;
381
+ },
382
+ finishAction(event, ctx) {
383
+ prune();
384
+ const toolName = clean2(event.toolName) ?? clean2(ctx.toolName);
385
+ if (!toolName || !DELIVERY_ACTION_TOOLS.has(toolName)) return false;
386
+ const toolCallId = clean2(event.toolCallId) ?? clean2(ctx.toolCallId);
387
+ const runId = (toolCallId ? runByToolCall.get(toolCallId) : void 0) ?? resolveRun(event, ctx);
388
+ if (!runId) return false;
389
+ const record = byRun.get(runId);
390
+ if (!record) return false;
391
+ const outcome = parseToolOutcome(event.result);
392
+ const succeeded = event.error === void 0 && hasCompleteSchedulingReceipts(outcome);
393
+ record.updatedAt = now();
394
+ record.actionFinished = true;
395
+ record.actionSucceeded = succeeded;
396
+ if (succeeded && outcome) {
397
+ record.userVisibleText = buildTransactionSuccessText(outcome);
398
+ delete record.errorCode;
399
+ } else {
400
+ record.errorCode = outcome?.ok === true && outcome?.dryRun === true ? "dry_run" : clean2(outcome?.error) ?? clean2(event.error) ?? (event.error === void 0 && outcome?.ok === true && outcome?.dryRun === false ? "transaction_receipt_invalid" : "transaction_failed");
401
+ delete record.userVisibleText;
402
+ }
403
+ return true;
404
+ },
405
+ // After the one authoritative action finished, every OTHER tool call in the
406
+ // same run is blocked (the action's own toolCallId is allowed through).
407
+ blockFurtherTool(event, ctx) {
408
+ prune();
409
+ const runId = resolveRun(event, ctx);
410
+ if (!runId) return false;
411
+ const record = byRun.get(runId);
412
+ if (!record?.actionFinished) return false;
413
+ const toolCallId = clean2(event.toolCallId) ?? clean2(ctx.toolCallId);
414
+ return !toolCallId || runByToolCall.get(toolCallId) !== runId;
415
+ },
416
+ suppressNonFinal(event) {
417
+ prune();
418
+ if (event.kind === "final") return false;
419
+ const runId = clean2(event.runId);
420
+ return Boolean(runId && byRun.has(runId));
421
+ },
422
+ expectedFinalText(runIdValue) {
423
+ prune();
424
+ const runId = clean2(runIdValue);
425
+ if (!runId) return void 0;
426
+ const record = byRun.get(runId);
427
+ if (!record?.actionAttempted) return void 0;
428
+ return record.actionSucceeded ? record.userVisibleText : record.userVisibleText ?? buildTransactionFailureText(record.errorCode);
429
+ },
430
+ expectedFinalTextForToolCall(toolCallIdValue) {
431
+ prune();
432
+ const toolCallId = clean2(toolCallIdValue);
433
+ const runId = toolCallId ? runByToolCall.get(toolCallId) : void 0;
434
+ return runId ? this.expectedFinalText(runId) : void 0;
435
+ },
436
+ pendingWithoutAction(runIdValue) {
437
+ prune();
438
+ const runId = clean2(runIdValue);
439
+ if (!runId) return false;
440
+ const record = byRun.get(runId);
441
+ return Boolean(record && !record.actionAttempted);
442
+ },
443
+ claimNoActionFailureForSession(sessionKeyValue) {
444
+ prune();
445
+ const sessionKey = clean2(sessionKeyValue);
446
+ if (!sessionKey) return void 0;
447
+ const record = [...byRun.values()].filter(
448
+ (candidate) => candidate.sessionKey === sessionKey && !candidate.actionAttempted && !candidate.finalTextClaimed
449
+ ).sort((left, right) => right.updatedAt - left.updatedAt)[0];
450
+ if (!record) return void 0;
451
+ record.finalTextClaimed = true;
452
+ record.updatedAt = now();
453
+ return "I didn't complete a scheduling transaction. No calendar event was created or changed.";
454
+ },
455
+ authoritativeFinalForSession(sessionKeyValue, modelClaimsSuccess) {
456
+ prune();
457
+ const sessionKey = clean2(sessionKeyValue);
458
+ if (!sessionKey) return void 0;
459
+ const record = [...byRun.values()].filter((candidate) => candidate.sessionKey === sessionKey && !candidate.finalDelivered).sort((left, right) => right.updatedAt - left.updatedAt)[0];
460
+ if (!record) return void 0;
461
+ if (record.actionAttempted) {
462
+ return record.actionSucceeded ? record.userVisibleText : record.userVisibleText ?? buildTransactionFailureText(record.errorCode);
463
+ }
464
+ return modelClaimsSuccess ? "I didn't complete a scheduling transaction. No calendar event was created or changed." : void 0;
465
+ },
466
+ markFinalDelivered(sessionKeyValue) {
467
+ prune();
468
+ const sessionKey = clean2(sessionKeyValue);
469
+ if (!sessionKey) return false;
470
+ const record = [...byRun.values()].filter((candidate) => candidate.sessionKey === sessionKey).sort((left, right) => right.updatedAt - left.updatedAt)[0];
471
+ if (!record) return false;
472
+ record.finalDelivered = true;
473
+ record.updatedAt = now();
474
+ return true;
475
+ },
476
+ finalAlreadyDelivered(sessionKeyValue) {
477
+ prune();
478
+ const sessionKey = clean2(sessionKeyValue);
479
+ if (!sessionKey) return false;
480
+ const latest = [...byRun.values()].filter((candidate) => candidate.sessionKey === sessionKey).sort((left, right) => right.updatedAt - left.updatedAt)[0];
481
+ return latest?.finalDelivered === true;
482
+ },
483
+ claimFinalTextForSession(sessionKeyValue) {
484
+ prune();
485
+ const sessionKey = clean2(sessionKeyValue);
486
+ if (!sessionKey) return void 0;
487
+ const record = [...byRun.values()].filter(
488
+ (candidate) => candidate.sessionKey === sessionKey && candidate.actionAttempted && !candidate.finalTextClaimed
489
+ ).sort((left, right) => right.updatedAt - left.updatedAt)[0];
490
+ if (!record) return void 0;
491
+ record.finalTextClaimed = true;
492
+ record.updatedAt = now();
493
+ return record.actionSucceeded ? record.userVisibleText : record.userVisibleText ?? buildTransactionFailureText(record.errorCode);
494
+ },
495
+ finalizeReply(event) {
496
+ prune();
497
+ if (event.kind !== "final") return void 0;
498
+ const runId = clean2(event.runId);
499
+ if (!runId) return void 0;
500
+ const record = byRun.get(runId);
501
+ if (!record) return void 0;
502
+ byRun.delete(runId);
503
+ for (const [toolCallId, candidateRunId] of runByToolCall) {
504
+ if (candidateRunId === runId) runByToolCall.delete(toolCallId);
505
+ }
506
+ if (!record.actionAttempted) return {};
507
+ return {
508
+ text: record.actionSucceeded ? record.userVisibleText : record.userVisibleText ?? buildTransactionFailureText(record.errorCode)
509
+ };
510
+ }
511
+ };
512
+ }
513
+
514
+ // src/injection.ts
515
+ var CONVERSATION_ROUTE_PREFIXES = /* @__PURE__ */ new Set([
516
+ "channel",
517
+ "chat",
518
+ "conversation",
519
+ "direct",
520
+ "dm",
521
+ "group",
522
+ "thread",
523
+ "user"
524
+ ]);
525
+ function canonicalizeConversationId(value, channel) {
526
+ let current = value?.trim();
527
+ if (!current) {
528
+ return void 0;
529
+ }
530
+ const normalizedChannel = channel?.trim().toLowerCase();
531
+ for (let depth = 0; depth < 2; depth += 1) {
532
+ const separator = current.indexOf(":");
533
+ if (separator < 0) {
534
+ break;
535
+ }
536
+ const prefix = current.slice(0, separator).trim().toLowerCase();
537
+ const suffix = current.slice(separator + 1).trim();
538
+ if (!suffix || !CONVERSATION_ROUTE_PREFIXES.has(prefix) && prefix !== normalizedChannel) {
539
+ break;
540
+ }
541
+ current = suffix;
542
+ }
543
+ return current;
544
+ }
545
+ function scopeMatches(itemValue, turnValue) {
546
+ if (itemValue === void 0) {
547
+ return true;
548
+ }
549
+ return turnValue !== void 0 && itemValue === turnValue;
550
+ }
551
+ function filterForTurn(items, turn, options) {
552
+ return items.filter((item) => {
553
+ if (options.strictUserScope && item.userId === void 0) {
554
+ return false;
555
+ }
556
+ if (options.strictSessionScope && item.sessionKey === void 0 && item.conversationId === void 0) {
557
+ return false;
558
+ }
559
+ const exactDestinationMatches = item.sessionKey !== void 0 ? scopeMatches(item.sessionKey, turn.sessionKey) : scopeMatches(
560
+ canonicalizeConversationId(item.conversationId, item.channel),
561
+ canonicalizeConversationId(turn.conversationId, turn.channel)
562
+ );
563
+ return scopeMatches(item.agentId, turn.agentId) && scopeMatches(item.channel, turn.channel) && scopeMatches(item.userId, turn.userId) && scopeMatches(item.accountId, turn.accountId) && exactDestinationMatches;
564
+ });
565
+ }
566
+ var HEADER = "[pending responsibility interactions]";
567
+ function itemLine(index, item) {
568
+ const slots = item.offeredSlots.length ? ` offered: ${item.offeredSlots.map((s, slotIndex) => {
569
+ const stableId = s.id ?? (s.number ? String(s.number) : `option-${slotIndex + 1}`);
570
+ return `${stableId}=${s.display}`;
571
+ }).join("; ")}` : item.stage === "needs_info" ? " awaiting a preferred time" : "";
572
+ return `${index}. id=${item.interactionId} \xB7 v=${item.stateVersion} \xB7 stage=${item.stage} \xB7 "${item.summary}"${slots}`;
573
+ }
574
+ function buildInjectionBlock(items, options) {
575
+ if (items.length === 0) {
576
+ return "";
577
+ }
578
+ const intro = `You have ${items.length} pending responsibility interaction${items.length === 1 ? "" : "s"} awaiting this user's reply. Delivery transcript mirrors are best-effort; pending state is authoritative. Before interpreting a scheduling reply, call responsibility_pending and bind it to exactly ONE interactionId. If more than one could match, ask which \u2014 never guess a thread or improvise provider actions. Interpret natural-language choices against the offered labels; when exactly one option matches, pass its stable optionId. If no option or multiple options match, ask one clarification question without invoking an action tool.`;
579
+ const footer = options.selectionEnabled === false ? "Selection writes are disabled. Use responsibility_pending or responsibility_status only; do not invoke runner effect modes." : "For a uniquely matched natural-language or numeric choice, act only via responsibility_select_option with its exact interactionId and stable optionId. Displayed numbers are also accepted as a compatibility fallback. Authorization and current state are bound internally; never supply a revision, time, message id, or invoke runner effect modes manually. After any typed action returns, stop the turn and give its one result; never retry or call another tool in the same turn.";
580
+ const shown = Math.min(items.length, Math.max(1, options.maxItems));
581
+ let lines = items.slice(0, shown).map((it, i) => itemLine(i + 1, it));
582
+ const hiddenByCount = items.length - shown;
583
+ const assemble = (bodyLines, hidden) => {
584
+ const parts = [HEADER, intro, ...bodyLines];
585
+ if (hidden > 0) {
586
+ parts.push(`(+${hidden} more \u2014 call responsibility_pending for the full list)`);
587
+ }
588
+ parts.push(footer);
589
+ return parts.join("\n");
590
+ };
591
+ let block = assemble(lines, hiddenByCount);
592
+ if (block.length <= options.maxChars) {
593
+ return block;
594
+ }
595
+ let visible = lines.length;
596
+ while (visible > 1) {
597
+ visible -= 1;
598
+ lines = lines.slice(0, visible);
599
+ block = assemble(lines, items.length - visible);
600
+ if (block.length <= options.maxChars) {
601
+ return block;
602
+ }
603
+ }
604
+ const compact = [
605
+ HEADER,
606
+ `(+${items.length} more) responsibility_pending; bind one.`,
607
+ "Select only via responsibility_select_option(interactionId, optionId); then stop."
608
+ ].join("\n");
609
+ return compact.length <= options.maxChars ? compact : "";
610
+ }
611
+
612
+ // src/runner-exec.ts
613
+ import { execFileSync } from "child_process";
614
+ function createRunnerExec() {
615
+ return (input) => {
616
+ const [file, ...args] = input.command;
617
+ if (!file) {
618
+ return { ok: false, error: "empty_command" };
619
+ }
620
+ try {
621
+ const stdout = execFileSync(file, args, {
622
+ cwd: input.cwd,
623
+ timeout: input.timeoutMs,
624
+ // Cap captured output; execFileSync throws ENOBUFS past this, which we
625
+ // surface as a truncation so partial provider payloads are never parsed.
626
+ maxBuffer: input.maxOutputBytes,
627
+ encoding: "utf8",
628
+ input: input.stdin,
629
+ stdio: [input.stdin === void 0 ? "ignore" : "pipe", "pipe", "ignore"]
630
+ });
631
+ if (stdout.length > input.maxOutputBytes) {
632
+ return { ok: true, stdout: stdout.slice(0, input.maxOutputBytes), truncated: true };
633
+ }
634
+ return { ok: true, stdout };
635
+ } catch (err) {
636
+ const e = err;
637
+ if (e.code === "ENOBUFS") {
638
+ return { ok: false, error: "output_truncated", truncated: true };
639
+ }
640
+ const stdout = typeof e.stdout === "string" ? e.stdout : Buffer.isBuffer(e.stdout) ? e.stdout.toString("utf8") : void 0;
641
+ return {
642
+ ok: false,
643
+ ...stdout ? { stdout: stdout.slice(0, input.maxOutputBytes) } : {},
644
+ error: `runner_exec_failed: ${e.code ?? e.message ?? "unknown"}`
645
+ };
646
+ }
647
+ };
648
+ }
649
+
650
+ // src/source.ts
651
+ import { createHash } from "crypto";
652
+
653
+ // src/redaction.ts
654
+ var SENSITIVE_PATTERNS = [
655
+ // Composio / One action + connection ids seen in the reference runner CONFIG.
656
+ /conn_mod_def::[A-Za-z0-9_::-]+/g,
657
+ /live::[A-Za-z0-9_-]+/g,
658
+ // API-key-ish tokens (ak_..., sk_..., pk_..., ghp_..., xoxb-..., Bearer ...).
659
+ /\bak_[A-Za-z0-9]{6,}/g,
660
+ /\b[sp]k_[A-Za-z0-9]{6,}/g,
661
+ /\bghp_[A-Za-z0-9]{20,}/g,
662
+ /\bxox[baprs]-[A-Za-z0-9-]{8,}/g,
663
+ /\bBearer\s+[A-Za-z0-9._-]{8,}/gi,
664
+ // JWT-shaped triples.
665
+ /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g,
666
+ // Reasoning-tag residue (MiniMax mm:think etc.) must never surface.
667
+ /<\/?think[^>]*>/gi,
668
+ /\bmm:think\b/gi
669
+ ];
670
+ var REDACTED = "[redacted]";
671
+ function redactText(input) {
672
+ if (input === void 0 || input === null) {
673
+ return "";
674
+ }
675
+ let text = String(input);
676
+ for (const pattern of SENSITIVE_PATTERNS) {
677
+ text = text.replace(pattern, REDACTED);
678
+ }
679
+ return text.replace(/\s+/g, " ").trim();
680
+ }
681
+
682
+ // src/source.ts
683
+ function shortHash(parts) {
684
+ return createHash("sha256").update(parts.map((p) => p ?? "").join("|")).digest("hex").slice(0, 16);
685
+ }
686
+ function normalizeSlots(raw) {
687
+ const source = raw.offeredSlots ?? raw.numberedSlots ?? raw.candidateSlots ?? raw.options ?? [];
688
+ const slots = [];
689
+ for (const entry of source) {
690
+ if (!entry) {
691
+ continue;
692
+ }
693
+ if (typeof entry === "string") {
694
+ const value = entry.trim();
695
+ if (value) {
696
+ slots.push({ start: value, display: redactText(value) || value });
697
+ }
698
+ continue;
699
+ }
700
+ const start = typeof entry.start === "string" && entry.start.trim() ? entry.start.trim() : typeof entry.value === "string" && entry.value.trim() ? entry.value.trim() : entry.value && typeof entry.value === "object" && typeof entry.value.startsAt === "string" && entry.value.startsAt.trim() ? entry.value.startsAt.trim() : "";
701
+ if (!start) {
702
+ continue;
703
+ }
704
+ const slotId = typeof entry.optionId === "string" && entry.optionId.trim() ? entry.optionId.trim() : typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : entry.number !== void 0 ? String(entry.number) : void 0;
705
+ const slotNumber = typeof entry.number === "number" && Number.isSafeInteger(entry.number) && entry.number > 0 ? entry.number : typeof entry.number === "string" && /^[1-9]\d*$/.test(entry.number.trim()) && Number.isSafeInteger(Number(entry.number.trim())) ? Number(entry.number.trim()) : void 0;
706
+ const display = entry.display ?? entry.label ?? start;
707
+ slots.push({
708
+ ...slotId ? { id: slotId } : {},
709
+ ...slotNumber ? { number: slotNumber } : {},
710
+ start,
711
+ display: redactText(display) || start
712
+ });
713
+ }
714
+ return slots;
715
+ }
716
+ function mapRawItem(raw, source) {
717
+ const interactionId = (raw.interactionId ?? raw.id ?? raw.threadKey ?? "").trim();
718
+ if (!interactionId) {
719
+ return null;
720
+ }
721
+ const stage = (raw.stage ?? "").trim() || "needs_info";
722
+ const offeredSlots = normalizeSlots(raw);
723
+ const summary = redactText(raw.summary ?? raw.subject ?? raw.counterpart ?? raw.lastAskedToUser ?? "(no subject)") || "(no subject)";
724
+ const note = raw.note ? redactText(raw.note) : void 0;
725
+ const lastAskedToUser = raw.lastAskedToUser ? redactText(raw.lastAskedToUser) : void 0;
726
+ const stateVersion = raw.stateVersion === void 0 || raw.stateVersion === null ? "" : String(raw.stateVersion).trim();
727
+ const effectiveStateVersion = stateVersion || shortHash([interactionId, stage, JSON.stringify(offeredSlots), summary, lastAskedToUser]);
728
+ return {
729
+ interactionId,
730
+ stateVersion: effectiveStateVersion,
731
+ stage,
732
+ responsibilityId: source.responsibilityId,
733
+ ...source.agentId ?? raw.agentId ? { agentId: source.agentId ?? raw.agentId } : {},
734
+ ...source.channel ?? raw.channel ? { channel: source.channel ?? raw.channel } : {},
735
+ ...source.userId ?? raw.userId ?? raw.managerUserId ? { userId: source.userId ?? raw.userId ?? raw.managerUserId } : {},
736
+ ...source.sessionKey ?? raw.sessionKey ? { sessionKey: source.sessionKey ?? raw.sessionKey } : {},
737
+ ...source.accountId ?? raw.accountId ? { accountId: source.accountId ?? raw.accountId } : {},
738
+ ...source.conversationId ?? raw.conversationId ? { conversationId: source.conversationId ?? raw.conversationId } : {},
739
+ summary,
740
+ offeredSlots,
741
+ ...raw.requestedWindowHint ? { requestedWindowHint: redactText(raw.requestedWindowHint) } : {},
742
+ ...lastAskedToUser ? { lastAskedToUser } : {},
743
+ ...raw.deliveryMessageId ?? raw.delivery?.messageId ? { deliveryMessageId: (raw.deliveryMessageId ?? raw.delivery?.messageId ?? "").trim() } : {},
744
+ ...note ? { note } : {},
745
+ source: source.id
746
+ };
747
+ }
748
+ function parsePendingEnvelope(stdout) {
749
+ const trimmed = stdout.trim();
750
+ if (!trimmed) {
751
+ return null;
752
+ }
753
+ try {
754
+ return JSON.parse(trimmed);
755
+ } catch {
756
+ const start = trimmed.indexOf("{");
757
+ const end = trimmed.lastIndexOf("}");
758
+ if (start >= 0 && end > start) {
759
+ try {
760
+ return JSON.parse(trimmed.slice(start, end + 1));
761
+ } catch {
762
+ return null;
763
+ }
764
+ }
765
+ return null;
766
+ }
767
+ }
768
+ function loadPendingFromSource(source, exec) {
769
+ let result;
770
+ try {
771
+ result = exec({
772
+ command: source.command,
773
+ cwd: source.workspacePath,
774
+ timeoutMs: source.timeoutMs,
775
+ maxOutputBytes: source.maxOutputBytes
776
+ });
777
+ } catch (err) {
778
+ return { items: [], error: `exec_failed: ${err.message}` };
779
+ }
780
+ if (!result.ok) {
781
+ return { items: [], error: result.error ?? "exec_failed" };
782
+ }
783
+ if (result.truncated) {
784
+ return { items: [], error: "output_truncated" };
785
+ }
786
+ const envelope = parsePendingEnvelope(result.stdout ?? "");
787
+ const packagedPending = envelope?.facts?.filter((fact) => fact?.kind === "pending").flatMap((fact) => Array.isArray(fact.payload?.pending) ? fact.payload.pending : []) ?? [];
788
+ const pending = Array.isArray(envelope?.pending) ? envelope.pending : packagedPending.length > 0 || envelope?.facts?.some((fact) => fact?.kind === "pending") ? packagedPending : null;
789
+ if (!envelope || envelope.ok === false || !pending) {
790
+ return { items: [], error: "unparseable_pending_output" };
791
+ }
792
+ const items = [];
793
+ for (const raw of pending) {
794
+ const mapped = mapRawItem(raw, source);
795
+ if (mapped) {
796
+ items.push(mapped);
797
+ }
798
+ }
799
+ return { items };
800
+ }
801
+ function loadPendingFromSources(sources, exec) {
802
+ const items = [];
803
+ const errors = {};
804
+ for (const source of sources) {
805
+ const res = loadPendingFromSource(source, exec);
806
+ if (res.error) {
807
+ errors[source.id] = res.error;
808
+ }
809
+ items.push(...res.items);
810
+ }
811
+ return { items, errors };
812
+ }
813
+ function actionError(error, message) {
814
+ return { ok: false, error: { ok: false, error, message } };
815
+ }
816
+ function actionRecordFromOutput(stdout) {
817
+ if (!stdout) return void 0;
818
+ const parsed = parsePendingEnvelope(stdout);
819
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
820
+ const direct = parsed;
821
+ const facts = Array.isArray(direct.facts) ? direct.facts : [];
822
+ const fact = facts.find((entry) => {
823
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
824
+ const kind = entry.kind;
825
+ return kind === "action" || kind === "transaction" || kind === "result";
826
+ });
827
+ if (fact?.payload && typeof fact.payload === "object" && !Array.isArray(fact.payload)) {
828
+ return fact.payload;
829
+ }
830
+ return direct;
831
+ }
832
+ function runnerErrorFromRecord(record) {
833
+ if (record.ok !== false) return void 0;
834
+ const error = typeof record.error === "string" ? record.error : typeof record.code === "string" ? record.code : "transaction_failed";
835
+ const message = typeof record.message === "string" && record.message.trim() ? record.message.trim() : "The responsibility runner rejected this transaction before any success could be confirmed.";
836
+ return { ok: false, error, message };
837
+ }
838
+ function executeAuthoritativeAction(source, request, exec) {
839
+ if (!source.actionCommand || source.actionCommand.length === 0) {
840
+ return actionError(
841
+ "source_unavailable",
842
+ "This responsibility has no reviewed action bridge. No scheduling transaction was started."
843
+ );
844
+ }
845
+ let result;
846
+ try {
847
+ result = exec({
848
+ command: source.actionCommand,
849
+ cwd: source.workspacePath,
850
+ timeoutMs: source.actionTimeoutMs ?? source.timeoutMs,
851
+ maxOutputBytes: source.maxOutputBytes,
852
+ stdin: JSON.stringify(request)
853
+ });
854
+ } catch (err) {
855
+ return actionError("source_unavailable", `The responsibility action bridge could not start: ${err.message}`);
856
+ }
857
+ if (result.truncated) {
858
+ return actionError("source_unavailable", "The responsibility action bridge returned truncated output; no success can be claimed.");
859
+ }
860
+ const outcome = actionRecordFromOutput(result.stdout);
861
+ if (outcome) {
862
+ const runnerError = runnerErrorFromRecord(outcome);
863
+ if (runnerError) return { ok: false, error: runnerError };
864
+ return { ok: true, outcome };
865
+ }
866
+ return actionError(
867
+ "source_unavailable",
868
+ result.error ? `The responsibility action bridge failed: ${result.error}` : "The responsibility action bridge returned no typed transaction receipt."
869
+ );
870
+ }
871
+
872
+ // src/interactions.ts
873
+ function buildIndex(items) {
874
+ const byId = /* @__PURE__ */ new Map();
875
+ const order = [];
876
+ for (const item of items) {
877
+ if (!byId.has(item.interactionId)) {
878
+ order.push(item.interactionId);
879
+ }
880
+ byId.set(item.interactionId, item);
881
+ }
882
+ return { order, byId };
883
+ }
884
+ function listInteractions(index) {
885
+ return index.order.map((id) => index.byId.get(id)).filter(Boolean);
886
+ }
887
+ var RESPONDABLE_STAGES = /* @__PURE__ */ new Set([
888
+ "awaiting_confirmation",
889
+ "awaiting_manager_selection",
890
+ "needs_info",
891
+ "needs_information",
892
+ "proposed"
893
+ ]);
894
+ var CONFIRMABLE_STAGES = /* @__PURE__ */ new Set([
895
+ "awaiting_confirmation",
896
+ "awaiting_manager_selection",
897
+ "proposed"
898
+ ]);
899
+ function isActionAllowed(interaction, action) {
900
+ switch (action) {
901
+ case "status":
902
+ return true;
903
+ case "select_option":
904
+ return CONFIRMABLE_STAGES.has(interaction.stage) && interaction.offeredSlots.length > 0;
905
+ case "reject":
906
+ case "change":
907
+ return RESPONDABLE_STAGES.has(interaction.stage);
908
+ default:
909
+ return false;
910
+ }
911
+ }
912
+ function selectInteraction(index, interactionId) {
913
+ const id = (interactionId ?? "").trim();
914
+ if (id) {
915
+ const interaction = index.byId.get(id);
916
+ if (!interaction) {
917
+ return {
918
+ ok: false,
919
+ error: "unknown_interaction",
920
+ message: `No pending interaction with id "${id}". Call responsibility_pending to list current ids.`
921
+ };
922
+ }
923
+ return { ok: true, interaction, autobound: false };
924
+ }
925
+ const candidates = index.order;
926
+ if (candidates.length === 0) {
927
+ return {
928
+ ok: false,
929
+ error: "no_pending_interactions",
930
+ message: "There are no pending interactions to act on. Check with the user before acting."
931
+ };
932
+ }
933
+ if (candidates.length > 1) {
934
+ return {
935
+ ok: false,
936
+ error: "ambiguous_requires_interaction_id",
937
+ message: "More than one interaction is pending. Re-issue with an explicit interactionId; do not guess which thread the reply refers to.",
938
+ candidates: [...candidates]
939
+ };
940
+ }
941
+ return { ok: true, interaction: index.byId.get(candidates[0]), autobound: true };
942
+ }
943
+
944
+ // src/tools.ts
945
+ var PENDING_CONTRACT = {
946
+ name: "responsibility_pending",
947
+ label: "List pending responsibility interactions",
948
+ readOnly: true,
949
+ description: "List the responsibility interactions currently visible to THIS authenticated user in THIS exact channel and session. Cron transcript mirrors are best-effort, so call this authoritative scoped-state tool before interpreting any scheduling reply. An empty result proves only that nothing is visible in this scope; never infer that another interaction completed, closed, expired, or timed out. If more than one could match, ask the user which \u2014 never guess.",
950
+ parameters: {
951
+ type: "object",
952
+ properties: {},
953
+ required: [],
954
+ additionalProperties: false
955
+ }
956
+ };
957
+ var SELECT_OPTION_CONTRACT = {
958
+ name: "responsibility_select_option",
959
+ label: "Select an offered option",
960
+ readOnly: false,
961
+ description: "Select one stable option already offered on a pending interaction. First interpret the user's natural-language reply against the authoritative offered labels. If exactly one option matches, call this tool with the exact interactionId and that option's stable optionId. If no option or multiple options match, ask one clarification question without calling this tool. A displayed option number is also accepted and resolved to a stable optionId internally. The current revision, selected time, sender, and inbound message are resolved from authoritative runtime state; never supply or invent them.",
962
+ parameters: {
963
+ type: "object",
964
+ properties: {
965
+ interactionId: { type: "string", description: "The interaction to select from responsibility_pending." },
966
+ optionId: {
967
+ type: "string",
968
+ description: "The stable offered option id, or the displayed one-based option number selected by the user."
969
+ }
970
+ },
971
+ required: ["interactionId", "optionId"],
972
+ additionalProperties: false
973
+ }
974
+ };
975
+ var REJECT_CONTRACT = {
976
+ name: "responsibility_reject",
977
+ label: "Reject a pending interaction",
978
+ readOnly: false,
979
+ description: "Record that the user declined a pending interaction. Requires the exact interactionId and current stateVersion and a short reason. Authorization is bound internally to the trusted inbound message. Dry-run by default.",
980
+ parameters: {
981
+ type: "object",
982
+ properties: {
983
+ interactionId: { type: "string", description: "The interaction to reject (from responsibility_pending)." },
984
+ stateVersion: { type: "string", description: "The interaction's current stateVersion; a stale value is rejected." },
985
+ reason: { type: "string", description: "Short user-facing reason for the rejection." }
986
+ },
987
+ required: ["interactionId", "stateVersion", "reason"],
988
+ additionalProperties: false
989
+ }
990
+ };
991
+ var CHANGE_CONTRACT = {
992
+ name: "responsibility_change",
993
+ label: "Request a different window for a pending interaction",
994
+ readOnly: false,
995
+ description: "Record that the user wants a different time window for a pending interaction. Requires the exact interactionId and current stateVersion plus the requested window. Authorization is bound internally to the trusted inbound message. Dry-run by default.",
996
+ parameters: {
997
+ type: "object",
998
+ properties: {
999
+ interactionId: { type: "string", description: "The interaction to change (from responsibility_pending)." },
1000
+ stateVersion: { type: "string", description: "The interaction's current stateVersion; a stale value is rejected." },
1001
+ requestedWindow: { type: "string", description: "The window the user is asking for (free text or ISO range)." }
1002
+ },
1003
+ required: ["interactionId", "stateVersion", "requestedWindow"],
1004
+ additionalProperties: false
1005
+ }
1006
+ };
1007
+ var STATUS_CONTRACT = {
1008
+ name: "responsibility_status",
1009
+ label: "Read a pending interaction's status",
1010
+ readOnly: true,
1011
+ description: "Read the current stage and offered slots of one pending interaction. Read-only.",
1012
+ parameters: {
1013
+ type: "object",
1014
+ properties: {
1015
+ interactionId: { type: "string", description: "The interaction to inspect (from responsibility_pending)." }
1016
+ },
1017
+ required: ["interactionId"],
1018
+ additionalProperties: false
1019
+ }
1020
+ };
1021
+ function readParam(params, key) {
1022
+ const value = params[key];
1023
+ return typeof value === "string" ? value.trim() : "";
1024
+ }
1025
+ var SCOPED_EMPTY_PENDING_TEXT = "No pending scheduling interactions are visible in this authenticated session. This scoped result does not prove that an interaction was completed, closed, expired, or timed out in another session. Check from the intended manager's authenticated channel.";
1026
+ function runPendingTool(index) {
1027
+ const items = listInteractions(index);
1028
+ const empty = items.length === 0;
1029
+ return {
1030
+ ok: true,
1031
+ pendingCount: items.length,
1032
+ pending: items.map((it) => ({
1033
+ interactionId: it.interactionId,
1034
+ stateVersion: it.stateVersion,
1035
+ stage: it.stage,
1036
+ summary: it.summary,
1037
+ offeredSlots: it.offeredSlots,
1038
+ ...it.requestedWindowHint ? { requestedWindowHint: it.requestedWindowHint } : {},
1039
+ ...it.lastAskedToUser ? { lastAskedToUser: it.lastAskedToUser } : {},
1040
+ ...it.note ? { note: it.note } : {}
1041
+ })),
1042
+ ...empty ? {
1043
+ visibility: "authenticated_session",
1044
+ absenceProof: "scoped_only",
1045
+ stopAfterThisTool: true,
1046
+ userVisibleText: SCOPED_EMPTY_PENDING_TEXT
1047
+ } : {},
1048
+ hint: empty ? "Return userVisibleText exactly. Do not call another tool, consult session history, or infer completion, closure, expiry, booking, resolution, or timeout." : "Bind the user's reply to exactly one interactionId. If more than one could match, ask which \u2014 do not guess."
1049
+ };
1050
+ }
1051
+ function runStatusTool(index, params) {
1052
+ const selected = selectInteraction(index, readParam(params, "interactionId"));
1053
+ if (selected.ok === false) {
1054
+ return selected;
1055
+ }
1056
+ const it = selected.interaction;
1057
+ return {
1058
+ ok: true,
1059
+ interactionId: it.interactionId,
1060
+ stateVersion: it.stateVersion,
1061
+ stage: it.stage,
1062
+ summary: it.summary,
1063
+ offeredSlots: it.offeredSlots,
1064
+ ...it.requestedWindowHint ? { requestedWindowHint: it.requestedWindowHint } : {},
1065
+ ...it.lastAskedToUser ? { lastAskedToUser: it.lastAskedToUser } : {},
1066
+ ...it.note ? { note: it.note } : {}
1067
+ };
1068
+ }
1069
+ function validateAction(index, action, params) {
1070
+ const selected = selectInteraction(index, readParam(params, "interactionId"));
1071
+ if (selected.ok === false) {
1072
+ return selected;
1073
+ }
1074
+ const interaction = selected.interaction;
1075
+ const userMessageId = readParam(params, "userMessageId");
1076
+ if (!userMessageId) {
1077
+ return {
1078
+ ok: false,
1079
+ error: "missing_user_message_id",
1080
+ message: "userMessageId is required so the action is bound to the message that authorized it."
1081
+ };
1082
+ }
1083
+ const stateVersion = readParam(params, "stateVersion");
1084
+ if (stateVersion !== interaction.stateVersion) {
1085
+ return {
1086
+ ok: false,
1087
+ error: "state_version_mismatch",
1088
+ message: `stateVersion "${stateVersion || "(missing)"}" does not match the interaction's current "${interaction.stateVersion}". Re-read responsibility_pending and retry with the current value.`
1089
+ };
1090
+ }
1091
+ if (!isActionAllowed(interaction, action)) {
1092
+ return {
1093
+ ok: false,
1094
+ error: "action_not_allowed_for_stage",
1095
+ message: `Action "${action}" is not allowed while interaction "${interaction.interactionId}" is in stage "${interaction.stage}"${action === "select_option" && interaction.offeredSlots.length === 0 ? " (no slots offered yet)" : ""}.`
1096
+ };
1097
+ }
1098
+ return { interaction };
1099
+ }
1100
+ function runSelectOptionTool(index, params, dryRun) {
1101
+ const selected = selectInteraction(index, readParam(params, "interactionId"));
1102
+ if (selected.ok === false) return selected;
1103
+ const interaction = selected.interaction;
1104
+ if (!isActionAllowed(interaction, "select_option")) {
1105
+ return {
1106
+ ok: false,
1107
+ error: "action_not_allowed_for_stage",
1108
+ message: `Option selection is not allowed while interaction "${interaction.interactionId}" is in stage "${interaction.stage}"${interaction.offeredSlots.length === 0 ? " (no options offered yet)" : ""}.`
1109
+ };
1110
+ }
1111
+ const userMessageId = readParam(params, "userMessageId");
1112
+ if (!userMessageId) {
1113
+ return {
1114
+ ok: false,
1115
+ error: "missing_user_message_id",
1116
+ message: "A trusted inbound message is required to select an option."
1117
+ };
1118
+ }
1119
+ const optionId = readParam(params, "optionId");
1120
+ if (!optionId) {
1121
+ return { ok: false, error: "missing_option", message: "optionId is required to select an option." };
1122
+ }
1123
+ let offered = interaction.offeredSlots.find((slot) => slot.id === optionId);
1124
+ if (!offered && /^[1-9]\d*$/.test(optionId)) {
1125
+ const displayedNumber = Number(optionId);
1126
+ if (Number.isSafeInteger(displayedNumber)) {
1127
+ const explicitlyNumbered = interaction.offeredSlots.filter(
1128
+ (slot) => slot.number === displayedNumber
1129
+ );
1130
+ if (explicitlyNumbered.length === 1) {
1131
+ offered = explicitlyNumbered[0];
1132
+ } else if (explicitlyNumbered.length === 0 && interaction.offeredSlots.every((slot) => slot.number === void 0)) {
1133
+ offered = interaction.offeredSlots[displayedNumber - 1];
1134
+ }
1135
+ }
1136
+ }
1137
+ if (!offered) {
1138
+ return {
1139
+ ok: false,
1140
+ error: "slot_not_offered",
1141
+ message: `optionId "${optionId}" is not one of interaction "${interaction.interactionId}"'s offered options. To propose a different time, the responsibility must offer new slots first.`
1142
+ };
1143
+ }
1144
+ const canonicalOptionId = offered.id ?? offered.start;
1145
+ return {
1146
+ ok: true,
1147
+ dryRun,
1148
+ action: "select_option",
1149
+ interactionId: interaction.interactionId,
1150
+ stateVersion: interaction.stateVersion,
1151
+ wouldTransitionTo: interaction.stage === "awaiting_manager_selection" ? "awaiting_counterparty" : "scheduled",
1152
+ wouldPerform: `Would record confirmation of ${redactText(offered.display) || offered.start} for "${interaction.summary}" and hand off to the responsibility worker to book + reply.`,
1153
+ accepted: {
1154
+ interactionId: interaction.interactionId,
1155
+ optionId: canonicalOptionId
1156
+ }
1157
+ };
1158
+ }
1159
+ function runRejectTool(index, params, dryRun) {
1160
+ const validated = validateAction(index, "reject", params);
1161
+ if ("ok" in validated && validated.ok === false) {
1162
+ return validated;
1163
+ }
1164
+ const { interaction } = validated;
1165
+ const reason = readParam(params, "reason");
1166
+ if (!reason) {
1167
+ return { ok: false, error: "missing_reason", message: "reason is required to reject an interaction." };
1168
+ }
1169
+ return {
1170
+ ok: true,
1171
+ dryRun,
1172
+ action: "reject",
1173
+ interactionId: interaction.interactionId,
1174
+ stateVersion: interaction.stateVersion,
1175
+ wouldTransitionTo: interaction.stage === "awaiting_manager_selection" ? "declined" : "closed",
1176
+ wouldPerform: `Would close "${interaction.summary}" as declined (${redactText(reason)}).`,
1177
+ accepted: { interactionId: interaction.interactionId, reason: redactText(reason) }
1178
+ };
1179
+ }
1180
+ function runChangeTool(index, params, dryRun) {
1181
+ const validated = validateAction(index, "change", params);
1182
+ if ("ok" in validated && validated.ok === false) {
1183
+ return validated;
1184
+ }
1185
+ const { interaction } = validated;
1186
+ const requestedWindow = readParam(params, "requestedWindow");
1187
+ if (!requestedWindow) {
1188
+ return { ok: false, error: "missing_requested_window", message: "requestedWindow is required to request a different time." };
1189
+ }
1190
+ return {
1191
+ ok: true,
1192
+ dryRun,
1193
+ action: "change",
1194
+ interactionId: interaction.interactionId,
1195
+ stateVersion: interaction.stateVersion,
1196
+ wouldTransitionTo: interaction.stage === "awaiting_manager_selection" ? "needs_information" : "needs_info",
1197
+ wouldPerform: `Would ask the responsibility to recompute availability for "${interaction.summary}" around ${redactText(requestedWindow)}.`,
1198
+ accepted: { interactionId: interaction.interactionId, requestedWindow: redactText(requestedWindow) }
1199
+ };
1200
+ }
1201
+
1202
+ // src/index.ts
1203
+ var PLUGIN_ID = "velanir-pending-context";
1204
+ var TRUSTED_ACTION_POLICY_ID = "authoritative-pending-action";
1205
+ var CACHE_TTL_MS = 3e3;
1206
+ function loadItems(config, exec, cache) {
1207
+ const now = Date.now();
1208
+ if (cache.value && now - cache.value.at < CACHE_TTL_MS) {
1209
+ return cache.value.items;
1210
+ }
1211
+ const { items } = loadPendingFromSources(config.sources, exec);
1212
+ cache.value = { at: now, items };
1213
+ return items;
1214
+ }
1215
+ function turnScopeFromContext(ctx) {
1216
+ const channel = ctx.channel ?? ctx.messageProvider;
1217
+ const conversationId = canonicalizeConversationId(ctx.chatId ?? ctx.channelId, channel);
1218
+ return {
1219
+ ...ctx.agentId ? { agentId: ctx.agentId } : {},
1220
+ ...channel ? { channel } : {},
1221
+ ...ctx.senderId ? { userId: ctx.senderId } : {},
1222
+ ...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {},
1223
+ ...conversationId ? { conversationId } : {}
1224
+ };
1225
+ }
1226
+ function toolScopeFromContext(ctx) {
1227
+ const channel = ctx.messageChannel ?? ctx.deliveryContext?.channel;
1228
+ const accountId = ctx.agentAccountId ?? ctx.deliveryContext?.accountId;
1229
+ const conversationId = canonicalizeConversationId(ctx.deliveryContext?.to, channel);
1230
+ return {
1231
+ ...ctx.agentId ? { agentId: ctx.agentId } : {},
1232
+ ...channel ? { channel } : {},
1233
+ ...ctx.requesterSenderId ? { userId: ctx.requesterSenderId } : {},
1234
+ ...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {},
1235
+ ...accountId ? { accountId } : {},
1236
+ ...conversationId ? { conversationId } : {}
1237
+ };
1238
+ }
1239
+ function toolResult(payload) {
1240
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], details: payload };
1241
+ }
1242
+ function scopedItems(config, exec, cache, scope) {
1243
+ return filterForTurn(loadItems(config, exec, cache), scope, {
1244
+ strictUserScope: config.strictUserScope,
1245
+ strictSessionScope: config.strictSessionScope
1246
+ });
1247
+ }
1248
+ function makeReadToolFactory(contract, config, exec, cache, run) {
1249
+ return (ctx) => {
1250
+ const scope = toolScopeFromContext(ctx);
1251
+ return {
1252
+ name: contract.name,
1253
+ label: contract.label,
1254
+ description: contract.description,
1255
+ parameters: contract.parameters,
1256
+ execute: async (_toolCallId, params) => toolResult(run(scopedItems(config, exec, cache, scope), params ?? {}))
1257
+ };
1258
+ };
1259
+ }
1260
+ function authorizationError(error) {
1261
+ return {
1262
+ ok: false,
1263
+ error: "authorization_unavailable",
1264
+ message: error === "message_replayed" ? "This inbound message has already authorized an action. No retry was started." : "This action was not securely bound to the current inbound message. No transaction was started."
1265
+ };
1266
+ }
1267
+ function actionRequestFromOutcome(outcome, params, authorization) {
1268
+ const request = {
1269
+ action: outcome.action,
1270
+ interactionId: outcome.interactionId,
1271
+ stateVersion: outcome.stateVersion,
1272
+ userMessageId: authorization.messageId,
1273
+ authorization: {
1274
+ senderId: authorization.senderId,
1275
+ sessionKey: authorization.sessionKey,
1276
+ messageId: authorization.messageId
1277
+ }
1278
+ };
1279
+ if (outcome.action === "select_option") request.optionId = outcome.accepted.optionId;
1280
+ if (outcome.action === "reject") request.reason = typeof params.reason === "string" ? params.reason : void 0;
1281
+ if (outcome.action === "change") {
1282
+ request.requestedWindow = typeof params.requestedWindow === "string" ? params.requestedWindow : void 0;
1283
+ }
1284
+ return request;
1285
+ }
1286
+ function makeActionToolFactory(contract, config, exec, cache, authorizationStore, deliveryStore, run) {
1287
+ return (ctx) => {
1288
+ const scope = toolScopeFromContext(ctx);
1289
+ return {
1290
+ name: contract.name,
1291
+ label: contract.label,
1292
+ description: contract.description,
1293
+ parameters: contract.parameters,
1294
+ execute: async (toolCallId, params) => {
1295
+ const hookContext = {
1296
+ toolName: contract.name,
1297
+ toolCallId,
1298
+ ...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {}
1299
+ };
1300
+ const consumed = authorizationStore.consumeDetailed(toolCallId, scope);
1301
+ if (!consumed.authorization) {
1302
+ const error = authorizationError(consumed.error);
1303
+ deliveryStore.finishAction({ toolName: contract.name, toolCallId, result: error }, hookContext);
1304
+ return toolResult(error);
1305
+ }
1306
+ const internalParams = {
1307
+ ...params ?? {},
1308
+ userMessageId: consumed.authorization.messageId
1309
+ };
1310
+ const items = scopedItems(config, exec, cache, scope);
1311
+ const outcome = run(items, internalParams, config.dryRun);
1312
+ if (!outcome.ok) {
1313
+ deliveryStore.finishAction({ toolName: contract.name, toolCallId, result: outcome }, hookContext);
1314
+ return toolResult(outcome);
1315
+ }
1316
+ if (config.dryRun) {
1317
+ deliveryStore.finishAction({ toolName: contract.name, toolCallId, result: outcome }, hookContext);
1318
+ const finalText = deliveryStore.expectedFinalTextForToolCall(toolCallId);
1319
+ return toolResult({ ...outcome, userVisibleText: finalText });
1320
+ }
1321
+ const interaction = items.find((candidate) => candidate.interactionId === outcome.interactionId);
1322
+ const source = interaction ? config.sources.find((candidate) => candidate.id === interaction.source) : void 0;
1323
+ if (!source) {
1324
+ const error = {
1325
+ ok: false,
1326
+ error: "source_unavailable",
1327
+ message: "The authoritative responsibility source is no longer available. No transaction was started."
1328
+ };
1329
+ deliveryStore.finishAction({ toolName: contract.name, toolCallId, result: error }, hookContext);
1330
+ return toolResult(error);
1331
+ }
1332
+ const execution = executeAuthoritativeAction(
1333
+ source,
1334
+ actionRequestFromOutcome(outcome, internalParams, consumed.authorization),
1335
+ exec
1336
+ );
1337
+ const result = execution.ok ? execution.outcome : execution.error;
1338
+ deliveryStore.finishAction({
1339
+ toolName: contract.name,
1340
+ toolCallId,
1341
+ result,
1342
+ ...execution.ok ? {} : { error: execution.error.error }
1343
+ }, hookContext);
1344
+ if (execution.ok && hasCompleteSchedulingReceipts(execution.outcome)) {
1345
+ return toolResult({
1346
+ ok: true,
1347
+ dryRun: false,
1348
+ action: outcome.action,
1349
+ interactionId: outcome.interactionId,
1350
+ stateVersion: outcome.stateVersion,
1351
+ userVisibleText: deliveryStore.expectedFinalTextForToolCall(toolCallId)
1352
+ });
1353
+ }
1354
+ return toolResult(execution.ok ? {
1355
+ ok: false,
1356
+ error: "transaction_receipt_invalid",
1357
+ message: "The runner did not return the complete receipt required to claim scheduling success."
1358
+ } : execution.error);
1359
+ }
1360
+ };
1361
+ };
1362
+ }
1363
+ function replyPayloadText(event) {
1364
+ const text = event.payload?.text;
1365
+ return typeof text === "string" && text.trim() ? text : void 0;
1366
+ }
1367
+ function replaceReplyPayloadText(event, text) {
1368
+ if (!event.payload) return void 0;
1369
+ return { payload: { ...event.payload, text } };
1370
+ }
1371
+ function isRecord2(value) {
1372
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1373
+ }
1374
+ function assistantMessageText(message) {
1375
+ if (message.role !== "assistant") return void 0;
1376
+ if (typeof message.content === "string") return message.content;
1377
+ if (!Array.isArray(message.content)) return void 0;
1378
+ return message.content.filter((part) => isRecord2(part) && part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
1379
+ }
1380
+ function withAssistantMessageText(message, text) {
1381
+ if (typeof message.content === "string") return { ...message, content: text };
1382
+ if (!Array.isArray(message.content)) return message;
1383
+ let replaced = false;
1384
+ const content = message.content.flatMap((part) => {
1385
+ if (isRecord2(part) && part.type === "text" && typeof part.text === "string") {
1386
+ if (replaced) return [];
1387
+ replaced = true;
1388
+ return [{ ...part, text }];
1389
+ }
1390
+ return [part];
1391
+ });
1392
+ return replaced ? { ...message, content } : message;
1393
+ }
1394
+ function registerPendingContextPlugin(api, exec, runtime = {}) {
1395
+ const config = normalizeConfig(api.pluginConfig);
1396
+ const cache = { value: null };
1397
+ const authorizationStore = runtime.authorization ?? createInboundAuthorizationStore(config.authorizationTtlMs, Date.now, sharedInboundAuthorizationState());
1398
+ const deliveryStore = runtime.delivery ?? createTransactionDeliveryStore(config.authorizationTtlMs, Date.now, sharedTransactionDeliveryState());
1399
+ api.registerTool(makeReadToolFactory(
1400
+ PENDING_CONTRACT,
1401
+ config,
1402
+ exec,
1403
+ cache,
1404
+ (items) => runPendingTool(buildIndex(items))
1405
+ ));
1406
+ api.registerTool(makeActionToolFactory(
1407
+ SELECT_OPTION_CONTRACT,
1408
+ config,
1409
+ exec,
1410
+ cache,
1411
+ authorizationStore,
1412
+ deliveryStore,
1413
+ (items, params, dryRun) => runSelectOptionTool(buildIndex(items), params, dryRun)
1414
+ ));
1415
+ api.registerTool(makeActionToolFactory(
1416
+ REJECT_CONTRACT,
1417
+ config,
1418
+ exec,
1419
+ cache,
1420
+ authorizationStore,
1421
+ deliveryStore,
1422
+ (items, params, dryRun) => runRejectTool(buildIndex(items), params, dryRun)
1423
+ ));
1424
+ api.registerTool(makeActionToolFactory(
1425
+ CHANGE_CONTRACT,
1426
+ config,
1427
+ exec,
1428
+ cache,
1429
+ authorizationStore,
1430
+ deliveryStore,
1431
+ (items, params, dryRun) => runChangeTool(buildIndex(items), params, dryRun)
1432
+ ));
1433
+ api.registerTool(makeReadToolFactory(
1434
+ STATUS_CONTRACT,
1435
+ config,
1436
+ exec,
1437
+ cache,
1438
+ (items, params) => runStatusTool(buildIndex(items), params)
1439
+ ));
1440
+ api.registerTrustedToolPolicy({
1441
+ id: TRUSTED_ACTION_POLICY_ID,
1442
+ description: "Binds pending responsibility actions to one trusted inbound message and blocks post-action retries.",
1443
+ evaluate: (event, ctx) => {
1444
+ const toolEvent = event;
1445
+ const toolContext = ctx;
1446
+ if (deliveryStore.blockFurtherTool(toolEvent, toolContext)) {
1447
+ return { block: true, blockReason: "pending_context_action_already_finished" };
1448
+ }
1449
+ const toolName = toolEvent.toolName ?? toolContext.toolName;
1450
+ if (!toolName || !ACTION_TOOLS.has(toolName)) return void 0;
1451
+ const action = deliveryStore.beginAction(toolEvent, toolContext);
1452
+ if (!action.tracked) {
1453
+ return { block: true, blockReason: "pending_context_missing_run_identity" };
1454
+ }
1455
+ if (action.duplicate) {
1456
+ return { block: true, blockReason: "pending_context_duplicate_action" };
1457
+ }
1458
+ if (!authorizationStore.bindTool(toolEvent, toolContext)) {
1459
+ deliveryStore.failAction(toolEvent, toolContext, "authorization_unavailable");
1460
+ return { block: true, blockReason: "pending_context_authorization_unavailable" };
1461
+ }
1462
+ return void 0;
1463
+ }
1464
+ });
1465
+ api.on("message_received", (event, ctx) => {
1466
+ authorizationStore.recordInbound(event, ctx);
1467
+ return void 0;
1468
+ });
1469
+ api.on("agent_turn_prepare", (event, ctx) => {
1470
+ void event;
1471
+ if (config.sources.length === 0) return void 0;
1472
+ const turnContext = ctx ?? {};
1473
+ const scope = turnScopeFromContext(turnContext);
1474
+ const items = scopedItems(config, exec, cache, scope);
1475
+ if (items.length === 0) return void 0;
1476
+ if (authorizationStore.hasFreshBinding(scope, turnContext.runId)) {
1477
+ deliveryStore.markPendingTurn(turnContext.runId, scope.sessionKey);
1478
+ }
1479
+ const block = buildInjectionBlock(items, {
1480
+ maxChars: config.maxInjectionChars,
1481
+ maxItems: config.maxItemsInInjection
1482
+ });
1483
+ if (!block) return void 0;
1484
+ if (config.logging.decisions) {
1485
+ api.logger?.info?.(
1486
+ `[${PLUGIN_ID}] injected ${items.length} pending interaction(s) for session=${scope.sessionKey ?? "?"}`
1487
+ );
1488
+ }
1489
+ return { appendContext: block };
1490
+ });
1491
+ api.on("reply_payload_sending", (event, ctx) => {
1492
+ const reply = event;
1493
+ const messageContext = ctx;
1494
+ const runId = reply.runId ?? messageContext.runId;
1495
+ const sessionKey = reply.sessionKey ?? messageContext.sessionKey;
1496
+ if (config.suppressNonFinalReplies && reply.kind !== "final" && deliveryStore.suppressNonFinal({ ...reply, runId })) {
1497
+ return { cancel: true, reason: "pending_context_suppressed_non_final" };
1498
+ }
1499
+ if (reply.kind !== "final" || !sessionKey) return void 0;
1500
+ if (deliveryStore.finalAlreadyDelivered(sessionKey)) {
1501
+ return { cancel: true, reason: "pending_context_duplicate_final" };
1502
+ }
1503
+ const modelText = replyPayloadText(reply);
1504
+ const replacement = deliveryStore.authoritativeFinalForSession(
1505
+ sessionKey,
1506
+ claimsSchedulingSuccess(modelText)
1507
+ );
1508
+ const delivered = deliveryStore.markFinalDelivered(sessionKey);
1509
+ if (replacement) return replaceReplyPayloadText(reply, replacement);
1510
+ if (!delivered) return void 0;
1511
+ return void 0;
1512
+ });
1513
+ api.on("before_message_write", (event, ctx) => {
1514
+ const write = event;
1515
+ const messageContext = ctx;
1516
+ const sessionKey = write.sessionKey ?? messageContext.sessionKey;
1517
+ if (!sessionKey || !isRecord2(write.message)) return void 0;
1518
+ const current = assistantMessageText(write.message);
1519
+ if (!current) return void 0;
1520
+ const replacement = deliveryStore.authoritativeFinalForSession(sessionKey, claimsSchedulingSuccess(current));
1521
+ if (!replacement || replacement === current) return void 0;
1522
+ return { message: withAssistantMessageText(write.message, replacement) };
1523
+ });
1524
+ }
1525
+ var index_default = definePluginEntry({
1526
+ id: PLUGIN_ID,
1527
+ name: "Velanir Pending Responsibility Context",
1528
+ description: "Injects scoped pending responsibility context and finalizes scheduling claims from trusted runner receipts.",
1529
+ register(api) {
1530
+ registerPendingContextPlugin(api, createRunnerExec());
1531
+ }
1532
+ });
1533
+ export {
1534
+ PLUGIN_ID,
1535
+ TRUSTED_ACTION_POLICY_ID,
1536
+ index_default as default,
1537
+ registerPendingContextPlugin,
1538
+ toolScopeFromContext,
1539
+ turnScopeFromContext
1540
+ };