@sayknow-cli/coding-agent 0.3.12 → 0.3.15
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/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/daemon-cli.d.ts +2 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/malloc-env-guard.d.ts +65 -0
- package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +3 -1
- package/dist/types/cli/session-picker.d.ts +4 -3
- package/dist/types/cli/update-cli.d.ts +22 -3
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/commands/daemon.d.ts +6 -2
- package/dist/types/commands/launch.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/commands/web-search.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +3 -3
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +9 -9
- package/dist/types/config/keybindings.d.ts +134 -129
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +35 -35
- package/dist/types/config/models-config-schema.d.ts +111 -111
- package/dist/types/config/settings-schema.d.ts +56 -16
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +79 -6
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/edit/modes/patch.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/goals/tools/goal-tool.d.ts +6 -6
- package/dist/types/i18n/messages/en.d.ts +8 -8
- package/dist/types/internal-urls/local-protocol.d.ts +13 -9
- package/dist/types/lsp/index.d.ts +9 -9
- package/dist/types/lsp/types.d.ts +9 -9
- package/dist/types/main.d.ts +66 -2
- package/dist/types/modes/acp/acp-agent.d.ts +19 -2
- package/dist/types/modes/acp/acp-mode.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
- package/dist/types/modes/components/pet-selector.d.ts +11 -0
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
- package/dist/types/modes/components/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +4 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +24 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
- package/dist/types/modes/theme/defaults/index.d.ts +568 -568
- package/dist/types/modes/types.d.ts +28 -2
- package/dist/types/modes/utils/abort-message.d.ts +1 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/modes/utils/irc-message.d.ts +38 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
- package/dist/types/notifications/config.d.ts +34 -0
- package/dist/types/notifications/daemon-paths.d.ts +7 -0
- package/dist/types/notifications/index.d.ts +27 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
- package/dist/types/notifications/notification-service.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
- package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +130 -3
- package/dist/types/notifications/telegram-reference.d.ts +19 -2
- package/dist/types/notifications/topic-registry.d.ts +24 -4
- package/dist/types/registry/agent-registry.d.ts +2 -0
- package/dist/types/rlm/index.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +40 -3
- package/dist/types/session/cache-economics.d.ts +79 -0
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/dist/types/session/provider-safety-stop.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +124 -4
- package/dist/types/session/session-storage.d.ts +155 -13
- package/dist/types/setup/credential-auto-import.d.ts +2 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/skc-runtime/state-schema.d.ts +19 -19
- package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/task/agents.d.ts +2 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +35 -27
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/browser.d.ts +28 -28
- package/dist/types/tools/computer.d.ts +20 -20
- package/dist/types/tools/cron.d.ts +2 -2
- package/dist/types/tools/debug.d.ts +40 -40
- package/dist/types/tools/gh.d.ts +10 -10
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/monitor.d.ts +6 -6
- package/dist/types/tools/skill-discovery.d.ts +4 -4
- package/dist/types/tools/subagent.d.ts +12 -12
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/tools/todo-write.d.ts +8 -8
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/dist/types/web/insane/bridge.d.ts +9 -9
- package/dist/types/web/search/index.d.ts +4 -4
- package/package.json +8 -8
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +51 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +14 -14
- package/src/config/settings-schema.ts +41 -1
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +2958 -430
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +301 -80
- package/src/modes/acp/acp-agent.ts +730 -100
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +32 -3
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/hook-selector.ts +12 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/sayknow-pet-widget.ts +360 -0
- package/src/modes/components/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/status-line/segments.ts +14 -7
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/controllers/command-controller.ts +28 -2
- package/src/modes/controllers/event-controller.ts +159 -36
- package/src/modes/controllers/extension-ui-controller.ts +37 -18
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +199 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +216 -50
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +635 -94
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/registry/agent-registry.ts +3 -0
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +670 -217
- package/src/session/cache-economics.ts +275 -0
- package/src/session/context-estimation.ts +91 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +809 -39
- package/src/session/session-storage.ts +582 -38
- package/src/skc-runtime/session-state-sidecar.ts +756 -135
- package/src/skc-runtime/tmux-common.ts +26 -7
- package/src/skc-runtime/tmux-gc.ts +52 -9
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/skc-runtime/tmux-sessions.ts +871 -68
- package/src/slash-commands/builtin-registry.ts +153 -9
- package/src/slash-commands/helpers/context-report.ts +26 -4
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/index.ts +81 -6
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
|
@@ -30,8 +30,22 @@ import { NotificationServer } from "@sayknow-cli/natives";
|
|
|
30
30
|
import { logger, postmortem } from "@sayknow-cli/utils";
|
|
31
31
|
import { Settings } from "../config/settings";
|
|
32
32
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../extensibility/extensions";
|
|
33
|
+
import type {
|
|
34
|
+
WorkflowGateEmitter,
|
|
35
|
+
WorkflowGateTerminalController,
|
|
36
|
+
} from "../modes/shared/agent-wire/unattended-session";
|
|
33
37
|
import { parseThinkingLevel } from "../thinking";
|
|
34
|
-
import {
|
|
38
|
+
import type {
|
|
39
|
+
AskAnswerRequest,
|
|
40
|
+
AskAnswerSourceResult,
|
|
41
|
+
AskRemoteControl,
|
|
42
|
+
AskRemoteInteraction,
|
|
43
|
+
AskRemoteReceipt,
|
|
44
|
+
AskSelectedAckOutcome,
|
|
45
|
+
AskSettlement,
|
|
46
|
+
AskSettlementResult,
|
|
47
|
+
} from "../tools";
|
|
48
|
+
import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "../tools/ask-answer-registry";
|
|
35
49
|
import { registerTelegramFileSink } from "./attachment-registry";
|
|
36
50
|
import {
|
|
37
51
|
getNotificationConfig,
|
|
@@ -327,8 +341,128 @@ async function readGitDiffStat(cwd: string): Promise<string | undefined> {
|
|
|
327
341
|
}
|
|
328
342
|
|
|
329
343
|
interface PendingInteractiveAsk {
|
|
330
|
-
resolve: (
|
|
344
|
+
resolve: (result: AskAnswerSourceResult) => void;
|
|
331
345
|
options: string[];
|
|
346
|
+
controls: readonly AskRemoteControl[];
|
|
347
|
+
reissue: () => boolean;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
interface UnattendedGatePresentation {
|
|
351
|
+
gateId: string;
|
|
352
|
+
sessionId: string;
|
|
353
|
+
question: string;
|
|
354
|
+
options: string[];
|
|
355
|
+
controls: readonly AskRemoteControl[];
|
|
356
|
+
multi: boolean;
|
|
357
|
+
allowEmpty: boolean;
|
|
358
|
+
navigationLabel?: "Next" | "Done";
|
|
359
|
+
selectedOptions: string[];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Keeps transport interaction ids separate from durable workflow gate ids. */
|
|
363
|
+
class GatePresentationRegistry {
|
|
364
|
+
private readonly presentations = new Map<string, UnattendedGatePresentation>();
|
|
365
|
+
private readonly routes = new Map<string, string>();
|
|
366
|
+
|
|
367
|
+
constructor(
|
|
368
|
+
private readonly server: NotificationServer,
|
|
369
|
+
private readonly redact: () => boolean,
|
|
370
|
+
private readonly tag: string,
|
|
371
|
+
) {}
|
|
372
|
+
|
|
373
|
+
retain(presentation: UnattendedGatePresentation): void {
|
|
374
|
+
this.presentations.set(presentation.gateId, presentation);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
routeFor(actionId: string): string | undefined {
|
|
378
|
+
return this.routes.get(actionId);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
presentationFor(actionId: string): UnattendedGatePresentation | undefined {
|
|
382
|
+
const gateId = this.routes.get(actionId);
|
|
383
|
+
return gateId ? this.presentations.get(gateId) : undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
toggle(actionId: string, label: string): boolean {
|
|
387
|
+
const presentation = this.presentationFor(actionId);
|
|
388
|
+
if (!presentation?.multi || !presentation.options.includes(label)) return false;
|
|
389
|
+
this.closeInteraction(actionId, "toggle");
|
|
390
|
+
const selected = new Set(presentation.selectedOptions);
|
|
391
|
+
if (selected.has(label)) selected.delete(label);
|
|
392
|
+
else selected.add(label);
|
|
393
|
+
presentation.selectedOptions = [...selected];
|
|
394
|
+
this.reissue(presentation.gateId);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
reissue(gateId: string): string | undefined {
|
|
399
|
+
const presentation = this.presentations.get(gateId);
|
|
400
|
+
if (!presentation) return undefined;
|
|
401
|
+
const actionId = `gate-interaction:${crypto.randomUUID()}`;
|
|
402
|
+
this.routes.set(actionId, gateId);
|
|
403
|
+
try {
|
|
404
|
+
this.server.registerAsk(
|
|
405
|
+
JSON.stringify(
|
|
406
|
+
notificationActionPayload(
|
|
407
|
+
{
|
|
408
|
+
id: actionId,
|
|
409
|
+
kind: "ask",
|
|
410
|
+
sessionId: presentation.sessionId,
|
|
411
|
+
question:
|
|
412
|
+
presentation.selectedOptions.length > 0
|
|
413
|
+
? `(${presentation.selectedOptions.length} selected) ${presentation.question}`
|
|
414
|
+
: presentation.question,
|
|
415
|
+
options: presentation.options,
|
|
416
|
+
controls: presentation.multi
|
|
417
|
+
? [
|
|
418
|
+
{
|
|
419
|
+
id: "navigation_forward",
|
|
420
|
+
kind: "navigation",
|
|
421
|
+
label: presentation.navigationLabel ?? "Done",
|
|
422
|
+
enabled: presentation.allowEmpty || presentation.selectedOptions.length > 0,
|
|
423
|
+
},
|
|
424
|
+
]
|
|
425
|
+
: [],
|
|
426
|
+
},
|
|
427
|
+
{ redact: this.redact(), sessionTag: this.tag },
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
true,
|
|
431
|
+
);
|
|
432
|
+
return actionId;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
this.routes.delete(actionId);
|
|
435
|
+
logger.warn(`notifications: registerAsk (gate interaction) failed: ${String(error)}`);
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
closeInteraction(actionId: string, reason: string): void {
|
|
441
|
+
this.routes.delete(actionId);
|
|
442
|
+
try {
|
|
443
|
+
this.server.resolveLocal(actionId, undefined);
|
|
444
|
+
} catch {
|
|
445
|
+
// The native claim close already terminalizes this action when applicable.
|
|
446
|
+
}
|
|
447
|
+
void reason;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
complete(gateId: string): void {
|
|
451
|
+
for (const [actionId, routeGateId] of this.routes) {
|
|
452
|
+
if (routeGateId !== gateId) continue;
|
|
453
|
+
this.closeInteraction(actionId, "gate_complete");
|
|
454
|
+
}
|
|
455
|
+
this.presentations.delete(gateId);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
cancel(gateId: string, reason: string): void {
|
|
459
|
+
this.complete(gateId);
|
|
460
|
+
void reason;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
dispose(): void {
|
|
464
|
+
for (const gateId of [...this.presentations.keys()]) this.cancel(gateId, "session_shutdown");
|
|
465
|
+
}
|
|
332
466
|
}
|
|
333
467
|
|
|
334
468
|
interface SessionRuntime {
|
|
@@ -342,6 +476,12 @@ interface SessionRuntime {
|
|
|
342
476
|
disposeFileSink: () => void;
|
|
343
477
|
/** Deregisters this session's unattended workflow-gate listener. */
|
|
344
478
|
disposeGateListener: () => void;
|
|
479
|
+
/** Deregisters canonical workflow-gate terminal cleanup. */
|
|
480
|
+
disposeGateTerminalController: () => void;
|
|
481
|
+
disposeAckRecoveryParticipant: () => void;
|
|
482
|
+
disposeGateEmitterListener: () => void;
|
|
483
|
+
workflowGate?: WorkflowGateEmitter;
|
|
484
|
+
gatePresentations?: GatePresentationRegistry;
|
|
345
485
|
redact: boolean;
|
|
346
486
|
verbosity: "lean" | "verbose";
|
|
347
487
|
sessionTag: string;
|
|
@@ -389,9 +529,11 @@ const defaultConfig: NotificationConfig = {
|
|
|
389
529
|
},
|
|
390
530
|
redact: false,
|
|
391
531
|
verbosity: "lean",
|
|
532
|
+
sessionScope: "all",
|
|
392
533
|
idleTimeoutMs: 60_000,
|
|
393
534
|
rich: { enabled: true },
|
|
394
535
|
richDraft: { enabled: false },
|
|
536
|
+
topics: {},
|
|
395
537
|
};
|
|
396
538
|
|
|
397
539
|
export function notificationsEnabled(): boolean {
|
|
@@ -463,18 +605,41 @@ function mapAnswerToLabel(answerJson: string, options: string[]): string | undef
|
|
|
463
605
|
return undefined;
|
|
464
606
|
}
|
|
465
607
|
|
|
466
|
-
/**
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
608
|
+
/** Workflow-gate answer shape (unattended mode). */
|
|
609
|
+
interface GateAnswer {
|
|
610
|
+
selected: string[];
|
|
611
|
+
other?: boolean;
|
|
612
|
+
custom?: string;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Discriminated result of mapping a client answer to a workflow-gate answer.
|
|
617
|
+
* `ok: false` means the reply is invalid and the caller must close the exact
|
|
618
|
+
* claim/receipt and reissue the interaction rather than durably accepting it.
|
|
619
|
+
*/
|
|
620
|
+
type GateAnswerResult = { ok: true; answer: GateAnswer } | { ok: false; reason: string };
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Map a client answer to the workflow-gate answer shape (unattended mode).
|
|
624
|
+
*
|
|
625
|
+
* The protocol defines a numeric reply as an option index, so a number outside
|
|
626
|
+
* `options` is invalid (issue #2030): it must NOT be converted into a free-text
|
|
627
|
+
* `Other` that passes the ask schema and triggers a misleading success ack.
|
|
628
|
+
* Only JSON strings enter the free-text/Other path.
|
|
629
|
+
*/
|
|
630
|
+
export function mapAnswerToGate(answerJson: string, options: string[]): GateAnswerResult {
|
|
471
631
|
const answer = parseAnswer(answerJson);
|
|
472
632
|
if (typeof answer === "number") {
|
|
473
633
|
const label = options[answer];
|
|
474
|
-
return label === undefined
|
|
634
|
+
return label === undefined
|
|
635
|
+
? { ok: false, reason: "numeric_selector_out_of_range" }
|
|
636
|
+
: { ok: true, answer: { selected: [label] } };
|
|
475
637
|
}
|
|
476
638
|
if (typeof answer === "string") {
|
|
477
|
-
return
|
|
639
|
+
return {
|
|
640
|
+
ok: true,
|
|
641
|
+
answer: options.includes(answer) ? { selected: [answer] } : { selected: [], other: true, custom: answer },
|
|
642
|
+
};
|
|
478
643
|
}
|
|
479
644
|
if (answer && typeof answer === "object") {
|
|
480
645
|
const obj = answer as { selected?: unknown; custom?: unknown };
|
|
@@ -482,9 +647,9 @@ function mapAnswerToGate(
|
|
|
482
647
|
? obj.selected.map(s => (typeof s === "number" ? (options[s] ?? String(s)) : String(s)))
|
|
483
648
|
: [];
|
|
484
649
|
const custom = typeof obj.custom === "string" ? obj.custom : undefined;
|
|
485
|
-
return { selected, other: custom !== undefined, custom };
|
|
650
|
+
return { ok: true, answer: { selected, other: custom !== undefined, custom } };
|
|
486
651
|
}
|
|
487
|
-
return { selected: [] };
|
|
652
|
+
return { ok: true, answer: { selected: [] } };
|
|
488
653
|
}
|
|
489
654
|
|
|
490
655
|
interface NotificationControlCommandPayload {
|
|
@@ -602,6 +767,92 @@ export async function executeNotificationControlCommand(
|
|
|
602
767
|
}
|
|
603
768
|
}
|
|
604
769
|
|
|
770
|
+
function selectedAckOutcome(value: { status: string; messageId?: number; reason?: string }): AskSelectedAckOutcome {
|
|
771
|
+
if (value.status === "delivered" && typeof value.messageId === "number") {
|
|
772
|
+
return { status: "delivered", messageId: value.messageId };
|
|
773
|
+
}
|
|
774
|
+
if (value.status === "failed") {
|
|
775
|
+
switch (value.reason) {
|
|
776
|
+
case "unsupported":
|
|
777
|
+
case "no_participant":
|
|
778
|
+
case "ambiguous_participant":
|
|
779
|
+
case "route_missing":
|
|
780
|
+
case "expired":
|
|
781
|
+
case "cancelled":
|
|
782
|
+
case "telegram_rejected":
|
|
783
|
+
case "session_closed":
|
|
784
|
+
return { status: "failed", reason: value.reason };
|
|
785
|
+
default:
|
|
786
|
+
return { status: "failed", reason: "session_closed" };
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
switch (value.reason) {
|
|
790
|
+
case "transport_ambiguous":
|
|
791
|
+
case "origin_disconnected":
|
|
792
|
+
case "host_timeout":
|
|
793
|
+
case "shutdown":
|
|
794
|
+
return { status: "unknown", reason: value.reason };
|
|
795
|
+
default:
|
|
796
|
+
return { status: "unknown", reason: "host_timeout" };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function requestLiveSelectedAck(
|
|
801
|
+
native: {
|
|
802
|
+
requestAskSelectedAck(
|
|
803
|
+
replyReceiptId: string,
|
|
804
|
+
requestJson: string,
|
|
805
|
+
): Promise<{ status: string; messageId?: number; reason?: string }>;
|
|
806
|
+
},
|
|
807
|
+
input: { replyReceiptId: string; actionId: string; commitKey: string; deadlineAt: number },
|
|
808
|
+
): Promise<AskSelectedAckOutcome> {
|
|
809
|
+
const requestId = `ack:${crypto.randomUUID()}`;
|
|
810
|
+
try {
|
|
811
|
+
return selectedAckOutcome(
|
|
812
|
+
await native.requestAskSelectedAck(
|
|
813
|
+
input.replyReceiptId,
|
|
814
|
+
JSON.stringify({
|
|
815
|
+
mode: "live",
|
|
816
|
+
requestId,
|
|
817
|
+
commitKey: input.commitKey,
|
|
818
|
+
actionId: input.actionId,
|
|
819
|
+
deadlineAt: input.deadlineAt,
|
|
820
|
+
}),
|
|
821
|
+
),
|
|
822
|
+
);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
logger.warn(`notifications: Selected acknowledgement failed: ${String(error)}`);
|
|
825
|
+
return { status: "unknown", reason: "host_timeout" };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function requestRecoveredSelectedAck(
|
|
830
|
+
native: {
|
|
831
|
+
requestRecoveredAskSelectedAck(
|
|
832
|
+
requestJson: string,
|
|
833
|
+
): Promise<{ status: string; messageId?: number; reason?: string }>;
|
|
834
|
+
},
|
|
835
|
+
input: { sessionId: string; actionId: string; commitKey: string; deadlineAt: number },
|
|
836
|
+
): Promise<AskSelectedAckOutcome> {
|
|
837
|
+
try {
|
|
838
|
+
return selectedAckOutcome(
|
|
839
|
+
await native.requestRecoveredAskSelectedAck(
|
|
840
|
+
JSON.stringify({
|
|
841
|
+
mode: "recovery",
|
|
842
|
+
requestId: `ack:${crypto.randomUUID()}`,
|
|
843
|
+
commitKey: input.commitKey,
|
|
844
|
+
sessionId: input.sessionId,
|
|
845
|
+
actionId: input.actionId,
|
|
846
|
+
deadlineAt: input.deadlineAt,
|
|
847
|
+
}),
|
|
848
|
+
),
|
|
849
|
+
);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
logger.warn(`notifications: recovered Selected acknowledgement failed: ${String(error)}`);
|
|
852
|
+
return { status: "unknown", reason: "host_timeout" };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
605
856
|
/** Register the interactive `ask` answer source for a session (the ask tool
|
|
606
857
|
* races the local UI against a remote reply). Returns the deregister disposer. */
|
|
607
858
|
function registerInteractiveAnswerSource(
|
|
@@ -613,29 +864,59 @@ function registerInteractiveAnswerSource(
|
|
|
613
864
|
): () => void {
|
|
614
865
|
return registerAskAnswerSource(id, {
|
|
615
866
|
awaitAnswer(question, options, signal) {
|
|
867
|
+
const result = this.awaitAnswerRequest?.({ question, options, interaction: "selector", controls: [] }, signal);
|
|
868
|
+
if (!result) return Promise.resolve(undefined);
|
|
869
|
+
return result.then(answer => {
|
|
870
|
+
if (!answer || typeof answer === "string") return answer;
|
|
871
|
+
return answer.interaction.kind === "value" ? answer.interaction.value : undefined;
|
|
872
|
+
});
|
|
873
|
+
},
|
|
874
|
+
awaitAnswerRequest(request: AskAnswerRequest, signal?: AbortSignal): Promise<AskAnswerSourceResult> {
|
|
616
875
|
if (signal?.aborted) return Promise.resolve(undefined);
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
876
|
+
const register = (askId: string): boolean => {
|
|
877
|
+
try {
|
|
878
|
+
server.registerAsk(
|
|
879
|
+
JSON.stringify(
|
|
880
|
+
notificationActionPayload(
|
|
881
|
+
{
|
|
882
|
+
id: askId,
|
|
883
|
+
kind: "ask",
|
|
884
|
+
sessionId: id,
|
|
885
|
+
question: request.question,
|
|
886
|
+
options: request.options,
|
|
887
|
+
controls: request.controls,
|
|
888
|
+
},
|
|
889
|
+
{ redact: getRedact(), sessionTag: tag },
|
|
890
|
+
),
|
|
624
891
|
),
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
892
|
+
true,
|
|
893
|
+
);
|
|
894
|
+
return true;
|
|
895
|
+
} catch (error) {
|
|
896
|
+
logger.warn(`notifications: registerAsk failed: ${String(error)}`);
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
let activeAskId = `ask:${crypto.randomUUID()}`;
|
|
901
|
+
if (!register(activeAskId)) return Promise.resolve(undefined);
|
|
902
|
+
return new Promise<AskAnswerSourceResult>(resolve => {
|
|
903
|
+
const pending: PendingInteractiveAsk = {
|
|
904
|
+
resolve,
|
|
905
|
+
options: request.options,
|
|
906
|
+
controls: request.controls,
|
|
907
|
+
reissue: () => {
|
|
908
|
+
const nextAskId = `ask:${crypto.randomUUID()}`;
|
|
909
|
+
if (!register(nextAskId)) return false;
|
|
910
|
+
activeAskId = nextAskId;
|
|
911
|
+
pendingInteractive.set(nextAskId, pending);
|
|
912
|
+
return true;
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
pendingInteractive.set(activeAskId, pending);
|
|
634
916
|
signal?.addEventListener("abort", () => {
|
|
635
|
-
if (!pendingInteractive.delete(
|
|
636
|
-
// Local UI answered: mark the remote action resolved-locally.
|
|
917
|
+
if (!pendingInteractive.delete(activeAskId)) return;
|
|
637
918
|
try {
|
|
638
|
-
server.resolveLocal(
|
|
919
|
+
server.resolveLocal(activeAskId, undefined);
|
|
639
920
|
} catch {}
|
|
640
921
|
resolve(undefined);
|
|
641
922
|
});
|
|
@@ -674,6 +955,16 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
674
955
|
try {
|
|
675
956
|
rt.disposeGateListener();
|
|
676
957
|
} catch {}
|
|
958
|
+
try {
|
|
959
|
+
rt.disposeGateTerminalController();
|
|
960
|
+
} catch {}
|
|
961
|
+
try {
|
|
962
|
+
rt.disposeAckRecoveryParticipant();
|
|
963
|
+
} catch {}
|
|
964
|
+
try {
|
|
965
|
+
rt.disposeGateEmitterListener();
|
|
966
|
+
} catch {}
|
|
967
|
+
rt.gatePresentations?.dispose();
|
|
677
968
|
// Resolve any still-pending interactive asks so the ask tool is not left hanging.
|
|
678
969
|
for (const pending of rt.pendingInteractive.values()) pending.resolve(undefined);
|
|
679
970
|
rt.pendingInteractive.clear();
|
|
@@ -708,11 +999,6 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
708
999
|
if (runtimes.has(id)) return "already";
|
|
709
1000
|
|
|
710
1001
|
const stateRoot = path.join(ctx.cwd, ".skc", "state");
|
|
711
|
-
const gate = ctx.workflowGate;
|
|
712
|
-
const unattended =
|
|
713
|
-
gate?.isUnattended?.() === true &&
|
|
714
|
-
typeof gate.onGateEmitted === "function" &&
|
|
715
|
-
typeof gate.resolveGate === "function";
|
|
716
1002
|
const gateOptions = new Map<string, string[]>();
|
|
717
1003
|
const pendingInteractive = new Map<string, PendingInteractiveAsk>();
|
|
718
1004
|
const tag = sessionTag(id);
|
|
@@ -723,41 +1009,161 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
723
1009
|
// The SDK can always answer now (interactive via the answer source, or the
|
|
724
1010
|
// unattended gate), so the endpoint advertises a resolver.
|
|
725
1011
|
const server = new NotificationServer(id, resolveToken(), stateRoot, true);
|
|
1012
|
+
const gatePresentations = new GatePresentationRegistry(server, () => runtime?.redact ?? redact, tag);
|
|
726
1013
|
|
|
727
1014
|
server.onReply((err, reply) => {
|
|
728
1015
|
if (err || !reply) return;
|
|
729
|
-
|
|
1016
|
+
const native = server as unknown as {
|
|
1017
|
+
resolveClaim(receiptId: string, answerJson?: string, idempotencyKey?: string): void;
|
|
1018
|
+
closeClaimInvalid(receiptId: string, reason: string): void;
|
|
1019
|
+
requestAskSelectedAck(
|
|
1020
|
+
receiptId: string,
|
|
1021
|
+
requestJson: string,
|
|
1022
|
+
): Promise<{ status: string; messageId?: number; reason?: string }>;
|
|
1023
|
+
};
|
|
730
1024
|
const pending = pendingInteractive.get(reply.id);
|
|
731
1025
|
if (pending) {
|
|
732
1026
|
pendingInteractive.delete(reply.id);
|
|
733
|
-
|
|
1027
|
+
let interaction: AskRemoteInteraction | undefined;
|
|
734
1028
|
try {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1029
|
+
const answer = JSON.parse(reply.answerJson) as unknown;
|
|
1030
|
+
if (typeof answer === "object" && answer && "controlId" in answer) {
|
|
1031
|
+
const controlId = (answer as { controlId?: unknown }).controlId;
|
|
1032
|
+
if (
|
|
1033
|
+
controlId === "navigation_forward" &&
|
|
1034
|
+
pending.controls.some(control => control.id === controlId && control.enabled)
|
|
1035
|
+
) {
|
|
1036
|
+
interaction = { kind: "control", controlId };
|
|
1037
|
+
}
|
|
1038
|
+
} else {
|
|
1039
|
+
const value = mapAnswerToLabel(reply.answerJson, pending.options);
|
|
1040
|
+
if (value !== undefined) interaction = { kind: "value", value };
|
|
1041
|
+
}
|
|
1042
|
+
} catch {}
|
|
1043
|
+
if (!interaction) {
|
|
1044
|
+
try {
|
|
1045
|
+
native.closeClaimInvalid(reply.replyReceiptId, "invalid_answer");
|
|
1046
|
+
} catch {}
|
|
1047
|
+
if (!pending.reissue()) pending.resolve(undefined);
|
|
1048
|
+
return;
|
|
738
1049
|
}
|
|
739
|
-
|
|
1050
|
+
let settled: Promise<AskSettlementResult> | undefined;
|
|
1051
|
+
const receipt: AskRemoteReceipt = {
|
|
1052
|
+
source: "remote",
|
|
1053
|
+
interaction,
|
|
1054
|
+
settle(settlement: AskSettlement): Promise<AskSettlementResult> {
|
|
1055
|
+
if (settled) return settled;
|
|
1056
|
+
settled = Promise.resolve().then(async () => {
|
|
1057
|
+
if (settlement.kind === "invalid") {
|
|
1058
|
+
native.closeClaimInvalid(reply.replyReceiptId, settlement.reason);
|
|
1059
|
+
return { kind: "invalid_closed" };
|
|
1060
|
+
}
|
|
1061
|
+
if (settlement.kind === "resolve_without_commit") {
|
|
1062
|
+
native.resolveClaim(reply.replyReceiptId, reply.answerJson, reply.idempotencyKey ?? undefined);
|
|
1063
|
+
return { kind: "resolved_without_commit" };
|
|
1064
|
+
}
|
|
1065
|
+
const ack = await requestLiveSelectedAck(native, {
|
|
1066
|
+
replyReceiptId: reply.replyReceiptId,
|
|
1067
|
+
actionId: reply.id,
|
|
1068
|
+
commitKey: `${reply.id}:${reply.idempotencyKey ?? reply.replyReceiptId}`,
|
|
1069
|
+
deadlineAt: Date.now() + 8_000,
|
|
1070
|
+
});
|
|
1071
|
+
native.resolveClaim(reply.replyReceiptId, reply.answerJson, reply.idempotencyKey ?? undefined);
|
|
1072
|
+
return { kind: "committed", ack };
|
|
1073
|
+
});
|
|
1074
|
+
return settled;
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
pending.resolve(receipt);
|
|
740
1078
|
return;
|
|
741
1079
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
gate
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1080
|
+
const gate = runtime?.workflowGate;
|
|
1081
|
+
const unattended =
|
|
1082
|
+
gate?.isUnattended?.() === true &&
|
|
1083
|
+
typeof gate.onGateEmitted === "function" &&
|
|
1084
|
+
typeof gate.resolveGate === "function";
|
|
1085
|
+
const gateId = gatePresentations.routeFor(reply.id);
|
|
1086
|
+
if (unattended && gateId && gate?.resolveGateFromNotification) {
|
|
1087
|
+
const presentation = gatePresentations.presentationFor(reply.id);
|
|
1088
|
+
const rawAnswer = parseAnswer(reply.answerJson);
|
|
1089
|
+
if (presentation?.multi) {
|
|
1090
|
+
const option =
|
|
1091
|
+
typeof rawAnswer === "number"
|
|
1092
|
+
? presentation.options[rawAnswer]
|
|
1093
|
+
: typeof rawAnswer === "string" && presentation.options.includes(rawAnswer)
|
|
1094
|
+
? rawAnswer
|
|
1095
|
+
: undefined;
|
|
1096
|
+
if (option !== undefined) {
|
|
1097
|
+
native.resolveClaim(reply.replyReceiptId, reply.answerJson, reply.idempotencyKey ?? undefined);
|
|
1098
|
+
if (!gatePresentations.toggle(reply.id, option)) gatePresentations.reissue(gateId);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
let answer: unknown;
|
|
1103
|
+
if (
|
|
1104
|
+
presentation?.multi &&
|
|
1105
|
+
typeof rawAnswer === "object" &&
|
|
1106
|
+
rawAnswer !== null &&
|
|
1107
|
+
(rawAnswer as { controlId?: unknown }).controlId === "navigation_forward"
|
|
1108
|
+
) {
|
|
1109
|
+
if (!presentation.allowEmpty && presentation.selectedOptions.length === 0) {
|
|
1110
|
+
native.closeClaimInvalid(reply.replyReceiptId, "invalid_control");
|
|
1111
|
+
gatePresentations.closeInteraction(reply.id, "invalid_control");
|
|
1112
|
+
gatePresentations.reissue(gateId);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
answer = { selected: presentation.selectedOptions };
|
|
1116
|
+
} else if (
|
|
1117
|
+
typeof rawAnswer === "object" &&
|
|
1118
|
+
rawAnswer !== null &&
|
|
1119
|
+
(rawAnswer as { action?: unknown }).action === "clarify"
|
|
1120
|
+
) {
|
|
1121
|
+
answer = rawAnswer;
|
|
1122
|
+
} else if (presentation?.multi && typeof rawAnswer === "string") {
|
|
1123
|
+
answer = { selected: presentation.selectedOptions, other: true, custom: rawAnswer };
|
|
1124
|
+
} else {
|
|
1125
|
+
const mapped = mapAnswerToGate(reply.answerJson, gateOptions.get(gateId) ?? []);
|
|
1126
|
+
if (!mapped.ok) {
|
|
1127
|
+
// A numeric selector outside options is invalid (issue #2030): close the
|
|
1128
|
+
// exact claim/receipt and reissue the interaction — never a success ack.
|
|
1129
|
+
native.closeClaimInvalid(reply.replyReceiptId, mapped.reason);
|
|
1130
|
+
gatePresentations.closeInteraction(reply.id, mapped.reason);
|
|
1131
|
+
gatePresentations.reissue(gateId);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
answer = mapped.answer;
|
|
1135
|
+
}
|
|
1136
|
+
void gate
|
|
1137
|
+
.resolveGateFromNotification(
|
|
1138
|
+
{ gate_id: gateId, answer, idempotency_key: reply.idempotencyKey ?? undefined },
|
|
1139
|
+
{
|
|
1140
|
+
interactionActionId: reply.id,
|
|
1141
|
+
replyReceiptId: reply.replyReceiptId,
|
|
1142
|
+
answerJson: reply.answerJson,
|
|
1143
|
+
idempotencyKey: reply.idempotencyKey ?? undefined,
|
|
1144
|
+
resolveClaim: () =>
|
|
1145
|
+
native.resolveClaim(reply.replyReceiptId, reply.answerJson, reply.idempotencyKey ?? undefined),
|
|
1146
|
+
closeClaimInvalid: reason => {
|
|
1147
|
+
native.closeClaimInvalid(reply.replyReceiptId, reason);
|
|
1148
|
+
gatePresentations.closeInteraction(reply.id, reason);
|
|
1149
|
+
gatePresentations.reissue(gateId);
|
|
1150
|
+
},
|
|
1151
|
+
requestSelectedAck: input =>
|
|
1152
|
+
requestLiveSelectedAck(native, {
|
|
1153
|
+
replyReceiptId: input.replyReceiptId,
|
|
1154
|
+
actionId: input.actionId,
|
|
1155
|
+
commitKey: input.commitKey,
|
|
1156
|
+
deadlineAt: input.daemonDeadlineAt,
|
|
1157
|
+
}),
|
|
1158
|
+
},
|
|
1159
|
+
)
|
|
1160
|
+
.catch(error => logger.warn(`notifications: resolveGateFromNotification failed: ${String(error)}`));
|
|
754
1161
|
return;
|
|
755
1162
|
}
|
|
756
|
-
// 3) No matching pending ask.
|
|
757
1163
|
try {
|
|
758
|
-
server.
|
|
759
|
-
} catch (
|
|
760
|
-
logger.warn(`notifications:
|
|
1164
|
+
server.closeClaimInvalid(reply.replyReceiptId, "unknown_action");
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
logger.warn(`notifications: closeClaimInvalid failed: ${String(error)}`);
|
|
761
1167
|
}
|
|
762
1168
|
});
|
|
763
1169
|
|
|
@@ -892,6 +1298,11 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
892
1298
|
disposeAnswerSource,
|
|
893
1299
|
disposeFileSink,
|
|
894
1300
|
disposeGateListener: () => {},
|
|
1301
|
+
disposeGateTerminalController: () => {},
|
|
1302
|
+
disposeAckRecoveryParticipant: () => {},
|
|
1303
|
+
disposeGateEmitterListener: () => {},
|
|
1304
|
+
workflowGate: undefined,
|
|
1305
|
+
gatePresentations,
|
|
895
1306
|
cancelPostmortemCleanup: () => {},
|
|
896
1307
|
redact,
|
|
897
1308
|
verbosity,
|
|
@@ -901,6 +1312,7 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
901
1312
|
pendingInbound: new Set<number>(),
|
|
902
1313
|
};
|
|
903
1314
|
runtimes.set(id, runtime);
|
|
1315
|
+
const activeRuntime = runtime;
|
|
904
1316
|
// A native terminal close (SIGHUP), SIGTERM, Ctrl+C exit, or fatal error
|
|
905
1317
|
// skips AgentSession.dispose(), so the `session_shutdown` extension event
|
|
906
1318
|
// never fires and the daemon-side topic would be orphaned. postmortem
|
|
@@ -910,7 +1322,7 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
910
1322
|
runtime.cancelPostmortemCleanup = postmortem.register(`notifications-session-closed:${id}`, async () => {
|
|
911
1323
|
await stopSession(id);
|
|
912
1324
|
});
|
|
913
|
-
logger.info(`notifications: serving session ${id} at ${endpoint.url}
|
|
1325
|
+
logger.info(`notifications: serving session ${id} at ${endpoint.url}`);
|
|
914
1326
|
|
|
915
1327
|
if (settingsAvailable && settings && isTelegramConfigured(cfg)) {
|
|
916
1328
|
try {
|
|
@@ -934,9 +1346,35 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
934
1346
|
logger.warn(`notifications: identity_header failed: ${String(e)}`);
|
|
935
1347
|
}
|
|
936
1348
|
|
|
937
|
-
//
|
|
938
|
-
|
|
939
|
-
|
|
1349
|
+
// Workflow-gate installation occurs after session_start in RPC/bridge modes.
|
|
1350
|
+
// Attach dynamically so those sessions receive the same Telegram gate surface.
|
|
1351
|
+
const attachWorkflowGate = (gate: WorkflowGateEmitter | undefined): void => {
|
|
1352
|
+
if (activeRuntime.workflowGate === gate) return;
|
|
1353
|
+
activeRuntime.disposeGateListener();
|
|
1354
|
+
activeRuntime.disposeGateTerminalController();
|
|
1355
|
+
activeRuntime.disposeAckRecoveryParticipant();
|
|
1356
|
+
activeRuntime.disposeGateListener = () => {};
|
|
1357
|
+
activeRuntime.disposeGateTerminalController = () => {};
|
|
1358
|
+
activeRuntime.disposeAckRecoveryParticipant = () => {};
|
|
1359
|
+
activeRuntime.workflowGate = undefined;
|
|
1360
|
+
gateOptions.clear();
|
|
1361
|
+
gatePresentations.dispose();
|
|
1362
|
+
if (typeof gate?.onGateEmitted !== "function" || typeof gate.resolveGate !== "function") {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
activeRuntime.workflowGate = gate;
|
|
1366
|
+
if (gate.registerGateTerminalController) {
|
|
1367
|
+
const controller: WorkflowGateTerminalController = {
|
|
1368
|
+
completeGateInteractions: gateId => gatePresentations.complete(gateId),
|
|
1369
|
+
cancelGateInteractions: (gateId, reason) => gatePresentations.cancel(gateId, reason),
|
|
1370
|
+
};
|
|
1371
|
+
try {
|
|
1372
|
+
activeRuntime.disposeGateTerminalController = gate.registerGateTerminalController(controller);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
logger.warn(`notifications: gate terminal controller unavailable: ${String(error)}`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
activeRuntime.disposeGateListener = gate.onGateEmitted(g => {
|
|
940
1378
|
const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
|
|
941
1379
|
gateOptions.set(g.gate_id, options);
|
|
942
1380
|
const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
|
|
@@ -944,21 +1382,43 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
944
1382
|
(typeof promptCtx?.prompt === "string" && promptCtx.prompt) ||
|
|
945
1383
|
(typeof promptCtx?.title === "string" && promptCtx.title) ||
|
|
946
1384
|
"Question";
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1385
|
+
const stageState =
|
|
1386
|
+
typeof g.context?.stage_state === "object" && g.context.stage_state !== null
|
|
1387
|
+
? (g.context.stage_state as Record<string, unknown>)
|
|
1388
|
+
: {};
|
|
1389
|
+
gatePresentations.retain({
|
|
1390
|
+
gateId: g.gate_id,
|
|
1391
|
+
sessionId: id,
|
|
1392
|
+
question,
|
|
1393
|
+
options,
|
|
1394
|
+
controls: [],
|
|
1395
|
+
multi: stageState.multi === true,
|
|
1396
|
+
allowEmpty: stageState.allow_empty === true,
|
|
1397
|
+
navigationLabel: stageState.navigation_label === "Next" ? "Next" : "Done",
|
|
1398
|
+
selectedOptions: [],
|
|
1399
|
+
});
|
|
1400
|
+
gatePresentations.reissue(g.gate_id);
|
|
960
1401
|
});
|
|
961
|
-
|
|
1402
|
+
if (gate.setAckRecoveryParticipant) {
|
|
1403
|
+
const native = server as unknown as {
|
|
1404
|
+
requestRecoveredAskSelectedAck(
|
|
1405
|
+
requestJson: string,
|
|
1406
|
+
): Promise<{ status: string; messageId?: number; reason?: string }>;
|
|
1407
|
+
};
|
|
1408
|
+
gate.setAckRecoveryParticipant({
|
|
1409
|
+
requestRecoveredAskSelectedAck: input =>
|
|
1410
|
+
requestRecoveredSelectedAck(native, {
|
|
1411
|
+
sessionId: input.sessionId,
|
|
1412
|
+
actionId: input.actionId,
|
|
1413
|
+
commitKey: input.commitKey,
|
|
1414
|
+
deadlineAt: input.deadlineAt,
|
|
1415
|
+
}),
|
|
1416
|
+
});
|
|
1417
|
+
activeRuntime.disposeAckRecoveryParticipant = () => gate.setAckRecoveryParticipant?.(null);
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
|
|
1421
|
+
if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
|
|
962
1422
|
return "started";
|
|
963
1423
|
} catch (e) {
|
|
964
1424
|
logger.warn(`notifications: failed to start server: ${String(e)}`);
|