@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
|
@@ -44,6 +44,8 @@ export interface AskGateQuestion {
|
|
|
44
44
|
deepInterview?: AskGateDeepInterviewState;
|
|
45
45
|
/** Override the emitted workflow gate address for non-deep-interview ask prompts. */
|
|
46
46
|
workflowGate?: AskGateWorkflowGateMeta;
|
|
47
|
+
allowEmpty?: boolean;
|
|
48
|
+
navigationLabel?: "Next" | "Done";
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface AskGateResult {
|
|
@@ -119,7 +121,7 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
|
|
|
119
121
|
const selectedBase: RpcJsonSchema = { type: "array", items: selectedItems, uniqueItems: true };
|
|
120
122
|
const selectedOnly: RpcJsonSchema = {
|
|
121
123
|
...selectedBase,
|
|
122
|
-
minItems: 1,
|
|
124
|
+
minItems: question.allowEmpty ? 0 : 1,
|
|
123
125
|
...(multi ? {} : { maxItems: 1 }),
|
|
124
126
|
};
|
|
125
127
|
const selectedWithOther: RpcJsonSchema = {
|
|
@@ -216,6 +218,8 @@ export function questionToGate(question: AskGateQuestion): OpenGateInput {
|
|
|
216
218
|
options: labels,
|
|
217
219
|
other_option: GATE_OTHER_OPTION,
|
|
218
220
|
clarification_action: "clarify",
|
|
221
|
+
allow_empty: question.allowEmpty === true,
|
|
222
|
+
navigation_label: question.navigationLabel,
|
|
219
223
|
...(question.deepInterview
|
|
220
224
|
? structuredDeepInterviewState(question.deepInterview)
|
|
221
225
|
: deepInterviewQuestionState(question.question)),
|
|
@@ -280,12 +284,21 @@ export function gateAnswerToResult(question: AskGateQuestion, answer: unknown):
|
|
|
280
284
|
}
|
|
281
285
|
const other = answer.other === true;
|
|
282
286
|
const totalPicks = deduped.length + (other ? 1 : 0);
|
|
283
|
-
if (totalPicks === 0) {
|
|
287
|
+
if (totalPicks === 0 && !question.allowEmpty) {
|
|
284
288
|
throw new DeepInterviewGateError(
|
|
285
289
|
"empty_selection",
|
|
286
290
|
"at least one option (or the free-text other) must be selected",
|
|
287
291
|
);
|
|
288
292
|
}
|
|
293
|
+
if (totalPicks === 0) {
|
|
294
|
+
return {
|
|
295
|
+
id: question.id,
|
|
296
|
+
question: question.question,
|
|
297
|
+
options: labels,
|
|
298
|
+
multi,
|
|
299
|
+
selectedOptions: [],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
289
302
|
if (!multi && totalPicks > 1) {
|
|
290
303
|
throw new DeepInterviewGateError("multi_not_allowed", "this question accepts a single selection");
|
|
291
304
|
}
|
|
@@ -302,6 +315,36 @@ export function gateAnswerToResult(question: AskGateQuestion, answer: unknown):
|
|
|
302
315
|
};
|
|
303
316
|
}
|
|
304
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Classify an accepted ask-domain gate answer independently of its transport.
|
|
320
|
+
* Schema-invalid and semantically invalid answers throw, so callers leave the
|
|
321
|
+
* durable gate pending rather than treating them as a non-committing answer.
|
|
322
|
+
*/
|
|
323
|
+
export function classifyAskGateDisposition(
|
|
324
|
+
gate: Pick<OpenGateInput, "context" | "options">,
|
|
325
|
+
answer: unknown,
|
|
326
|
+
): "commit" | "resolve_without_commit" {
|
|
327
|
+
const state = gate.context?.stage_state;
|
|
328
|
+
if (typeof state !== "object" || state === null) return "commit";
|
|
329
|
+
const questionId = (state as Record<string, unknown>).question_id;
|
|
330
|
+
if (typeof questionId !== "string") return "commit";
|
|
331
|
+
const question: AskGateQuestion = {
|
|
332
|
+
id: questionId,
|
|
333
|
+
question: typeof gate.context?.prompt === "string" ? gate.context.prompt : questionId,
|
|
334
|
+
options: (gate.options ?? []).map(option => ({
|
|
335
|
+
label: typeof option.value === "string" ? option.value : option.label,
|
|
336
|
+
})),
|
|
337
|
+
multi: (state as Record<string, unknown>).multi === true,
|
|
338
|
+
allowEmpty: (state as Record<string, unknown>).allow_empty === true,
|
|
339
|
+
};
|
|
340
|
+
if (typeof answer === "object" && answer !== null && (answer as Record<string, unknown>).action === "clarify") {
|
|
341
|
+
gateAnswerToResult(question, answer);
|
|
342
|
+
return "resolve_without_commit";
|
|
343
|
+
}
|
|
344
|
+
const result = gateAnswerToResult(question, answer);
|
|
345
|
+
return result.selectedOptions.length === 0 && result.customInput === undefined ? "resolve_without_commit" : "commit";
|
|
346
|
+
}
|
|
347
|
+
|
|
305
348
|
/** Convenience: map a batch of ask questions to gate open-inputs. */
|
|
306
349
|
export function questionsToGates(questions: AskGateQuestion[]): OpenGateInput[] {
|
|
307
350
|
return questions.map(questionToGate);
|
|
@@ -49,6 +49,7 @@ const RPC_COMMAND_SCOPE_REGISTRY: Record<RpcCommandType, BridgeCommandScope> = {
|
|
|
49
49
|
get_pending_workflow_gates: "message:read",
|
|
50
50
|
set_capabilities: "control",
|
|
51
51
|
set_model: "model",
|
|
52
|
+
set_default_model_selection: "model",
|
|
52
53
|
cycle_model: "model",
|
|
53
54
|
get_available_models: "model",
|
|
54
55
|
set_thinking_level: "model",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* RPC server can route `negotiate_unattended` + `workflow_gate_response` here.
|
|
16
16
|
*/
|
|
17
17
|
import type { Model } from "@sayknow-cli/ai";
|
|
18
|
+
import type { AskSelectedAckOutcome } from "../../../tools";
|
|
18
19
|
import type {
|
|
19
20
|
RpcCommand,
|
|
20
21
|
RpcUnattendedAccepted,
|
|
@@ -30,7 +31,53 @@ import {
|
|
|
30
31
|
type UnattendedAuditEvent,
|
|
31
32
|
UnattendedRunController,
|
|
32
33
|
} from "./unattended-run-controller";
|
|
33
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
type GateStore,
|
|
36
|
+
MemoryGateStore,
|
|
37
|
+
type OpenGateInput,
|
|
38
|
+
type PersistedAckPolicy,
|
|
39
|
+
type PersistedGate,
|
|
40
|
+
type PersistedSemanticDisposition,
|
|
41
|
+
WorkflowGateBroker,
|
|
42
|
+
} from "./workflow-gate-broker";
|
|
43
|
+
|
|
44
|
+
export interface WorkflowGateTerminalController {
|
|
45
|
+
completeGateInteractions(gateId: string): void | Promise<void>;
|
|
46
|
+
cancelGateInteractions(gateId: string, reason: string): void | Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WorkflowGateTerminalEvent {
|
|
50
|
+
gateId: string;
|
|
51
|
+
kind: "accepted" | "cancelled";
|
|
52
|
+
origin?: "rpc" | "bridge" | "other" | "telegram_notification";
|
|
53
|
+
reason?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface NotificationGateResolutionOptions {
|
|
57
|
+
interactionActionId: string;
|
|
58
|
+
replyReceiptId: string;
|
|
59
|
+
answerJson: string;
|
|
60
|
+
idempotencyKey?: string;
|
|
61
|
+
requestSelectedAck(input: {
|
|
62
|
+
replyReceiptId: string;
|
|
63
|
+
actionId: string;
|
|
64
|
+
commitKey: string;
|
|
65
|
+
daemonDeadlineAt: number;
|
|
66
|
+
hostTimeoutMs: number;
|
|
67
|
+
}): Promise<AskSelectedAckOutcome>;
|
|
68
|
+
resolveClaim(): void;
|
|
69
|
+
closeClaimInvalid(reason: string): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AskSelectedAckRecoveryParticipant {
|
|
73
|
+
requestRecoveredAskSelectedAck(input: {
|
|
74
|
+
sessionId: string;
|
|
75
|
+
actionId: string;
|
|
76
|
+
commitKey: string;
|
|
77
|
+
deadlineAt: number;
|
|
78
|
+
hostTimeoutMs: number;
|
|
79
|
+
}): Promise<AskSelectedAckOutcome>;
|
|
80
|
+
}
|
|
34
81
|
|
|
35
82
|
/**
|
|
36
83
|
* RPC commands that perform agent/tool work and therefore consume one unit of the
|
|
@@ -66,7 +113,13 @@ export interface WorkflowGateEmitter {
|
|
|
66
113
|
*/
|
|
67
114
|
onGateEmitted?(listener: (gate: RpcWorkflowGate) => void): () => void;
|
|
68
115
|
resolveGate?(response: RpcWorkflowGateResponse): Promise<RpcWorkflowGateResolution>;
|
|
116
|
+
resolveGateFromNotification?(
|
|
117
|
+
response: RpcWorkflowGateResponse,
|
|
118
|
+
options: NotificationGateResolutionOptions,
|
|
119
|
+
): Promise<RpcWorkflowGateResolution>;
|
|
120
|
+
registerGateTerminalController?(controller: WorkflowGateTerminalController): () => void;
|
|
69
121
|
listPendingGates?(): RpcWorkflowGate[];
|
|
122
|
+
setAckRecoveryParticipant?(participant: AskSelectedAckRecoveryParticipant | null): void;
|
|
70
123
|
}
|
|
71
124
|
|
|
72
125
|
export interface UnattendedSessionOptions {
|
|
@@ -91,6 +144,12 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
91
144
|
readonly #pending = new Map<string, { resolve: (answer: unknown) => void; reject: (err: Error) => void }>();
|
|
92
145
|
readonly #earlyAnswers = new Map<string, unknown>();
|
|
93
146
|
readonly #gateListeners = new Set<(gate: RpcWorkflowGate) => void>();
|
|
147
|
+
#terminalController: WorkflowGateTerminalController | undefined;
|
|
148
|
+
#brokerReady = false;
|
|
149
|
+
#recoveryPromise: Promise<void> | undefined;
|
|
150
|
+
#disposed = false;
|
|
151
|
+
#recoveryParticipant: AskSelectedAckRecoveryParticipant | undefined;
|
|
152
|
+
#participantReady = Promise.withResolvers<void>();
|
|
94
153
|
|
|
95
154
|
constructor(private readonly opts: UnattendedSessionOptions) {}
|
|
96
155
|
|
|
@@ -108,6 +167,38 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
108
167
|
return this.#controller;
|
|
109
168
|
}
|
|
110
169
|
|
|
170
|
+
registerGateTerminalController(controller: WorkflowGateTerminalController): () => void {
|
|
171
|
+
if (this.#terminalController && this.#terminalController !== controller) {
|
|
172
|
+
throw new Error("a workflow gate terminal controller is already registered");
|
|
173
|
+
}
|
|
174
|
+
this.#terminalController = controller;
|
|
175
|
+
return () => {
|
|
176
|
+
if (this.#terminalController === controller) this.#terminalController = undefined;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setAckRecoveryParticipant(participant: AskSelectedAckRecoveryParticipant | null): void {
|
|
181
|
+
this.#recoveryParticipant = participant ?? undefined;
|
|
182
|
+
if (participant) this.#participantReady.resolve();
|
|
183
|
+
void this.startRecoveryOnce();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async #completeGateInteractions(gateId: string): Promise<void> {
|
|
187
|
+
try {
|
|
188
|
+
await this.#terminalController?.completeGateInteractions(gateId);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.opts.audit?.({ event: "gate_interaction_completion_failed", gate_id: gateId, error: String(error) });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async #cancelGateInteractions(gateId: string, reason: string): Promise<void> {
|
|
195
|
+
try {
|
|
196
|
+
await this.#terminalController?.cancelGateInteractions(gateId, reason);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.opts.audit?.({ event: "gate_interaction_cancellation_failed", gate_id: gateId, error: String(error) });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
111
202
|
negotiate(declaration: RpcUnattendedDeclaration): RpcUnattendedAccepted {
|
|
112
203
|
const controller = UnattendedRunController.negotiate(declaration, {
|
|
113
204
|
runId: this.opts.runId,
|
|
@@ -140,7 +231,11 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
140
231
|
// openGate returns and the caller starts awaiting.
|
|
141
232
|
this.#earlyAnswers.set(gate.gate_id, answer);
|
|
142
233
|
},
|
|
234
|
+
finalizeAccepted: record => this.#finalizeAcceptedGate(record),
|
|
143
235
|
});
|
|
236
|
+
this.#brokerReady = true;
|
|
237
|
+
void this.startRecoveryOnce();
|
|
238
|
+
|
|
144
239
|
return {
|
|
145
240
|
run_id: this.opts.runId,
|
|
146
241
|
actor: controller.actor,
|
|
@@ -190,12 +285,203 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
190
285
|
}
|
|
191
286
|
|
|
192
287
|
resolveGate(response: RpcWorkflowGateResponse): Promise<RpcWorkflowGateResolution> {
|
|
288
|
+
if (this.#disposed) return Promise.reject(new Error("unattended session is disposed"));
|
|
193
289
|
if (!this.#broker) {
|
|
194
290
|
return Promise.reject(new Error("workflow gates are not available until unattended mode is negotiated"));
|
|
195
291
|
}
|
|
196
292
|
return this.#broker.resolve(response);
|
|
197
293
|
}
|
|
198
294
|
|
|
295
|
+
async resolveGateFromNotification(
|
|
296
|
+
response: RpcWorkflowGateResponse,
|
|
297
|
+
options: NotificationGateResolutionOptions,
|
|
298
|
+
): Promise<RpcWorkflowGateResolution> {
|
|
299
|
+
let claimSettled = false;
|
|
300
|
+
const resolveClaim = () => {
|
|
301
|
+
if (claimSettled) return;
|
|
302
|
+
claimSettled = true;
|
|
303
|
+
try {
|
|
304
|
+
options.resolveClaim();
|
|
305
|
+
} catch (error) {
|
|
306
|
+
this.opts.audit?.({
|
|
307
|
+
event: "notification_claim_resolution_failed",
|
|
308
|
+
gate_id: response.gate_id,
|
|
309
|
+
error: String(error),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
const closeClaimInvalid = (reason: string) => {
|
|
314
|
+
if (claimSettled) return;
|
|
315
|
+
claimSettled = true;
|
|
316
|
+
try {
|
|
317
|
+
options.closeClaimInvalid(reason);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
this.opts.audit?.({
|
|
320
|
+
event: "notification_claim_close_failed",
|
|
321
|
+
gate_id: response.gate_id,
|
|
322
|
+
error: String(error),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
if (this.#disposed) {
|
|
327
|
+
closeClaimInvalid("unattended_session_disposed");
|
|
328
|
+
throw new Error("unattended session is disposed");
|
|
329
|
+
}
|
|
330
|
+
if (!this.#broker) {
|
|
331
|
+
closeClaimInvalid("workflow_gate_unavailable");
|
|
332
|
+
throw new Error("workflow gates are not available until unattended mode is negotiated");
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
JSON.parse(options.answerJson);
|
|
336
|
+
} catch {
|
|
337
|
+
closeClaimInvalid("invalid_structured_answer");
|
|
338
|
+
return {
|
|
339
|
+
gate_id: response.gate_id,
|
|
340
|
+
status: "rejected",
|
|
341
|
+
answer_hash: "",
|
|
342
|
+
resolved_at: new Date().toISOString(),
|
|
343
|
+
error: this.#broker.validationError(response.gate_id, "json_parse", "invalid structured answer"),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
let disposition: PersistedSemanticDisposition;
|
|
347
|
+
try {
|
|
348
|
+
disposition = this.#broker.classifyDisposition(response.gate_id, response.answer);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
closeClaimInvalid(error instanceof Error ? error.message : "invalid_answer");
|
|
351
|
+
return {
|
|
352
|
+
gate_id: response.gate_id,
|
|
353
|
+
status: "rejected",
|
|
354
|
+
answer_hash: "",
|
|
355
|
+
resolved_at: new Date().toISOString(),
|
|
356
|
+
error: this.#broker.validationError(
|
|
357
|
+
response.gate_id,
|
|
358
|
+
"semantic_disposition",
|
|
359
|
+
error instanceof Error ? error.message : "invalid answer",
|
|
360
|
+
),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const commitKey = `${response.gate_id}:${options.idempotencyKey ?? options.replyReceiptId}`;
|
|
365
|
+
const ackPolicy: PersistedAckPolicy =
|
|
366
|
+
disposition === "commit"
|
|
367
|
+
? {
|
|
368
|
+
kind: "telegram_selected_v1",
|
|
369
|
+
commitKey,
|
|
370
|
+
actionId: options.interactionActionId,
|
|
371
|
+
state: "pending",
|
|
372
|
+
updatedAt: new Date().toISOString(),
|
|
373
|
+
}
|
|
374
|
+
: { kind: "none", reason: "semantic_noncommit" };
|
|
375
|
+
let resolution: RpcWorkflowGateResolution;
|
|
376
|
+
try {
|
|
377
|
+
resolution = await this.#broker.resolve(response, {
|
|
378
|
+
resolutionOrigin: { kind: "telegram_notification", interactionActionId: options.interactionActionId },
|
|
379
|
+
ackPolicy,
|
|
380
|
+
semanticDisposition: disposition,
|
|
381
|
+
|
|
382
|
+
beforeAdvance: async () => {
|
|
383
|
+
if (ackPolicy.kind === "telegram_selected_v1") {
|
|
384
|
+
this.#broker?.updateAckPolicy(response.gate_id, {
|
|
385
|
+
...ackPolicy,
|
|
386
|
+
state: "attempt_started",
|
|
387
|
+
updatedAt: new Date().toISOString(),
|
|
388
|
+
});
|
|
389
|
+
let outcome: AskSelectedAckOutcome;
|
|
390
|
+
try {
|
|
391
|
+
outcome = await options.requestSelectedAck({
|
|
392
|
+
replyReceiptId: options.replyReceiptId,
|
|
393
|
+
actionId: options.interactionActionId,
|
|
394
|
+
commitKey,
|
|
395
|
+
daemonDeadlineAt: Date.now() + 8_000,
|
|
396
|
+
hostTimeoutMs: 10_000,
|
|
397
|
+
});
|
|
398
|
+
} catch {
|
|
399
|
+
outcome = { status: "unknown", reason: "host_timeout" };
|
|
400
|
+
}
|
|
401
|
+
this.#broker?.updateAckPolicy(response.gate_id, {
|
|
402
|
+
...ackPolicy,
|
|
403
|
+
state: outcome.status,
|
|
404
|
+
outcome,
|
|
405
|
+
updatedAt: new Date().toISOString(),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
resolveClaim();
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
} catch (error) {
|
|
412
|
+
closeClaimInvalid(error instanceof Error ? error.message : "invalid_answer");
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
if (resolution.status === "accepted") resolveClaim();
|
|
416
|
+
else closeClaimInvalid(resolution.error?.code ?? "invalid_answer");
|
|
417
|
+
return resolution;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async startRecoveryOnce(options: { participantGraceMs?: number } = {}): Promise<void> {
|
|
421
|
+
if (this.#disposed || !this.#brokerReady) return;
|
|
422
|
+
if (this.#recoveryPromise) return this.#recoveryPromise;
|
|
423
|
+
this.#recoveryPromise = (async () => {
|
|
424
|
+
if (!this.#recoveryParticipant) {
|
|
425
|
+
const grace = options.participantGraceMs ?? 2_000;
|
|
426
|
+
await Promise.race([
|
|
427
|
+
this.#participantReady.promise,
|
|
428
|
+
new Promise<void>(resolve => setTimeout(resolve, grace)),
|
|
429
|
+
]);
|
|
430
|
+
}
|
|
431
|
+
if (!this.#disposed) await this.#broker?.recover();
|
|
432
|
+
})();
|
|
433
|
+
return this.#recoveryPromise;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async #finalizeAcceptedGate(record: PersistedGate): Promise<void> {
|
|
437
|
+
const policy = record.ackPolicy;
|
|
438
|
+
if (policy?.kind === "telegram_selected_v1" && policy.state === "pending") {
|
|
439
|
+
let outcome: AskSelectedAckOutcome;
|
|
440
|
+
const participant = this.#recoveryParticipant;
|
|
441
|
+
if (!participant || !this.opts.sessionId) {
|
|
442
|
+
outcome = { status: "failed", reason: "no_participant" };
|
|
443
|
+
} else {
|
|
444
|
+
this.#broker?.updateAckPolicy(record.gate.gate_id, {
|
|
445
|
+
...policy,
|
|
446
|
+
state: "attempt_started",
|
|
447
|
+
updatedAt: new Date().toISOString(),
|
|
448
|
+
});
|
|
449
|
+
try {
|
|
450
|
+
outcome = await participant.requestRecoveredAskSelectedAck({
|
|
451
|
+
sessionId: this.opts.sessionId,
|
|
452
|
+
actionId: policy.actionId,
|
|
453
|
+
commitKey: policy.commitKey,
|
|
454
|
+
deadlineAt: Date.now() + 8_000,
|
|
455
|
+
hostTimeoutMs: 10_000,
|
|
456
|
+
});
|
|
457
|
+
} catch {
|
|
458
|
+
outcome = { status: "unknown", reason: "host_timeout" };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
this.#broker?.updateAckPolicy(record.gate.gate_id, {
|
|
462
|
+
...policy,
|
|
463
|
+
state: outcome.status,
|
|
464
|
+
outcome,
|
|
465
|
+
updatedAt: new Date().toISOString(),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (policy?.kind === "telegram_selected_v1" && policy.state === "attempt_started") {
|
|
469
|
+
this.#broker?.updateAckPolicy(record.gate.gate_id, {
|
|
470
|
+
...policy,
|
|
471
|
+
state: "unknown",
|
|
472
|
+
outcome: { status: "unknown", reason: "shutdown" },
|
|
473
|
+
updatedAt: new Date().toISOString(),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
await this.#completeGateInteractions(record.gate.gate_id);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
dispose(reason = "session_shutdown"): void {
|
|
480
|
+
this.#disposed = true;
|
|
481
|
+
for (const gateId of this.#pending.keys()) void this.#cancelGateInteractions(gateId, reason);
|
|
482
|
+
this.#rejectAllPending(new Error(`unattended session disposed: ${reason}`));
|
|
483
|
+
}
|
|
484
|
+
|
|
199
485
|
listPendingGates(): import("../../rpc/rpc-types").RpcWorkflowGate[] {
|
|
200
486
|
const store = this.opts.store;
|
|
201
487
|
if (!store) return [];
|
|
@@ -206,6 +492,7 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
206
492
|
}
|
|
207
493
|
|
|
208
494
|
emitGate(input: OpenGateInput): Promise<unknown> {
|
|
495
|
+
if (this.#disposed) return Promise.reject(new Error("unattended session is disposed"));
|
|
209
496
|
if (!this.#broker) {
|
|
210
497
|
return Promise.reject(new Error("cannot emit a workflow gate before unattended mode is negotiated"));
|
|
211
498
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
+
import type { AskSelectedAckOutcome } from "../../../tools";
|
|
16
17
|
import type {
|
|
17
18
|
RpcWorkflowGate,
|
|
18
19
|
RpcWorkflowGateContext,
|
|
@@ -20,11 +21,36 @@ import type {
|
|
|
20
21
|
RpcWorkflowGateOption,
|
|
21
22
|
RpcWorkflowGateResolution,
|
|
22
23
|
RpcWorkflowGateResponse,
|
|
24
|
+
RpcWorkflowGateValidationError,
|
|
23
25
|
RpcWorkflowStage,
|
|
24
26
|
} from "../../rpc/rpc-types";
|
|
25
27
|
import { RESERVED_WORKFLOW_STAGES } from "../../rpc/rpc-types";
|
|
28
|
+
import { classifyAskGateDisposition } from "./deep-interview-gate";
|
|
26
29
|
import { answerHashOf, canonicalJson, compileGateSchema, schemaHash, validateGateAnswer } from "./workflow-gate-schema";
|
|
27
30
|
|
|
31
|
+
export type PersistedSemanticDisposition = "commit" | "resolve_without_commit";
|
|
32
|
+
export type PersistedResolutionOrigin =
|
|
33
|
+
| { kind: "generic"; channel: "rpc" | "bridge" | "other" }
|
|
34
|
+
| { kind: "telegram_notification"; interactionActionId: string }
|
|
35
|
+
| { kind: "legacy_unknown" };
|
|
36
|
+
export type PersistedAckPolicy =
|
|
37
|
+
| { kind: "none"; reason: "non_telegram" | "semantic_noncommit" | "legacy_unproven" }
|
|
38
|
+
| {
|
|
39
|
+
kind: "telegram_selected_v1";
|
|
40
|
+
commitKey: string;
|
|
41
|
+
actionId: string;
|
|
42
|
+
state: "pending" | "attempt_started" | "delivered" | "failed" | "unknown";
|
|
43
|
+
outcome?: AskSelectedAckOutcome;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface GateResolutionOptions {
|
|
48
|
+
semanticDisposition?: PersistedSemanticDisposition;
|
|
49
|
+
resolutionOrigin?: PersistedResolutionOrigin;
|
|
50
|
+
ackPolicy?: PersistedAckPolicy;
|
|
51
|
+
beforeAdvance?: () => Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
28
54
|
const V1_STAGES: readonly RpcWorkflowStage[] = ["deep-interview", "ralplan", "ultragoal"];
|
|
29
55
|
|
|
30
56
|
export interface PersistedGate {
|
|
@@ -36,6 +62,9 @@ export interface PersistedGate {
|
|
|
36
62
|
answer?: unknown;
|
|
37
63
|
resolution?: RpcWorkflowGateResolution;
|
|
38
64
|
advanced: boolean;
|
|
65
|
+
semanticDisposition?: PersistedSemanticDisposition;
|
|
66
|
+
resolutionOrigin?: PersistedResolutionOrigin;
|
|
67
|
+
ackPolicy?: PersistedAckPolicy;
|
|
39
68
|
}
|
|
40
69
|
|
|
41
70
|
export interface GateStore {
|
|
@@ -153,6 +182,9 @@ export interface BrokerHooks {
|
|
|
153
182
|
* it for any gate left `accepted` but not `advanced` by a crash.
|
|
154
183
|
*/
|
|
155
184
|
advance?(gate: RpcWorkflowGate, answer: unknown): void | Promise<void>;
|
|
185
|
+
/** Runs after durable acceptance and before advance, including crash recovery. */
|
|
186
|
+
finalizeAccepted?(record: PersistedGate): Promise<void>;
|
|
187
|
+
|
|
156
188
|
/** Append-only audit sink. */
|
|
157
189
|
audit?(event: GateAuditEvent): void;
|
|
158
190
|
}
|
|
@@ -176,12 +208,51 @@ export class WorkflowGateBrokerError extends Error {
|
|
|
176
208
|
}
|
|
177
209
|
|
|
178
210
|
export class WorkflowGateBroker {
|
|
211
|
+
private readonly recovering = new Set<string>();
|
|
212
|
+
private readonly gateLocks = new Map<string, Promise<void>>();
|
|
213
|
+
|
|
214
|
+
private async withGateLock<T>(gateId: string, operation: () => Promise<T>): Promise<T> {
|
|
215
|
+
const previous = this.gateLocks.get(gateId) ?? Promise.resolve();
|
|
216
|
+
const release = Promise.withResolvers<void>();
|
|
217
|
+
const current = previous.then(() => release.promise);
|
|
218
|
+
this.gateLocks.set(gateId, current);
|
|
219
|
+
await previous;
|
|
220
|
+
try {
|
|
221
|
+
return await operation();
|
|
222
|
+
} finally {
|
|
223
|
+
release.resolve();
|
|
224
|
+
if (this.gateLocks.get(gateId) === current) this.gateLocks.delete(gateId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
179
228
|
constructor(
|
|
180
229
|
private readonly runId: string,
|
|
181
230
|
private readonly store: GateStore,
|
|
182
231
|
private readonly hooks: BrokerHooks = {},
|
|
183
232
|
) {}
|
|
184
233
|
|
|
234
|
+
classifyDisposition(gateId: string, answer: unknown): PersistedSemanticDisposition {
|
|
235
|
+
const record = this.store.get(gateId);
|
|
236
|
+
if (!record) throw new WorkflowGateBrokerError("unknown_gate", `no pending gate ${gateId}`);
|
|
237
|
+
return classifyAskGateDisposition(record.gate, answer);
|
|
238
|
+
}
|
|
239
|
+
validationError(gateId: string, keyword: string, message: string): RpcWorkflowGateValidationError {
|
|
240
|
+
const record = this.store.get(gateId);
|
|
241
|
+
return {
|
|
242
|
+
code: "invalid_workflow_gate_answer",
|
|
243
|
+
gate_id: gateId,
|
|
244
|
+
schema_hash: record ? schemaHash(record.gate.schema) : "",
|
|
245
|
+
errors: [{ path: "$", keyword, message }],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
updateAckPolicy(gateId: string, ackPolicy: PersistedAckPolicy): void {
|
|
250
|
+
const record = this.store.get(gateId);
|
|
251
|
+
if (record?.status !== "accepted")
|
|
252
|
+
throw new WorkflowGateBrokerError("unknown_gate", `no accepted gate ${gateId}`);
|
|
253
|
+
this.store.put({ ...record, ackPolicy });
|
|
254
|
+
}
|
|
255
|
+
|
|
185
256
|
private runShort(): string {
|
|
186
257
|
return this.runId.replace(/[^a-zA-Z0-9]/g, "").slice(-8) || "run";
|
|
187
258
|
}
|
|
@@ -222,7 +293,17 @@ export class WorkflowGateBroker {
|
|
|
222
293
|
* On success the resolution is persisted BEFORE `advance` is invoked exactly
|
|
223
294
|
* once. Invalid answers leave the gate pending (per #315 acceptance).
|
|
224
295
|
*/
|
|
225
|
-
async resolve(
|
|
296
|
+
async resolve(
|
|
297
|
+
response: RpcWorkflowGateResponse,
|
|
298
|
+
options: GateResolutionOptions = {},
|
|
299
|
+
): Promise<RpcWorkflowGateResolution> {
|
|
300
|
+
return this.withGateLock(response.gate_id, () => this.resolveUnlocked(response, options));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async resolveUnlocked(
|
|
304
|
+
response: RpcWorkflowGateResponse,
|
|
305
|
+
options: GateResolutionOptions = {},
|
|
306
|
+
): Promise<RpcWorkflowGateResolution> {
|
|
226
307
|
const record = this.store.get(response.gate_id);
|
|
227
308
|
if (!record) {
|
|
228
309
|
this.hooks.audit?.({ event: "gate_response_unknown_gate", gate_id: response.gate_id });
|
|
@@ -263,6 +344,13 @@ export class WorkflowGateBroker {
|
|
|
263
344
|
};
|
|
264
345
|
}
|
|
265
346
|
|
|
347
|
+
const semanticDisposition =
|
|
348
|
+
options.semanticDisposition ?? classifyAskGateDisposition(record.gate, response.answer);
|
|
349
|
+
const resolutionOrigin = options.resolutionOrigin ?? { kind: "generic", channel: "rpc" as const };
|
|
350
|
+
const ackPolicy =
|
|
351
|
+
options.ackPolicy ??
|
|
352
|
+
({ kind: "none", reason: semanticDisposition === "commit" ? "non_telegram" : "semantic_noncommit" } as const);
|
|
353
|
+
|
|
266
354
|
const resolution: RpcWorkflowGateResolution = {
|
|
267
355
|
gate_id: response.gate_id,
|
|
268
356
|
status: "accepted",
|
|
@@ -280,18 +368,30 @@ export class WorkflowGateBroker {
|
|
|
280
368
|
answer: response.answer,
|
|
281
369
|
resolution,
|
|
282
370
|
advanced: false,
|
|
371
|
+
semanticDisposition,
|
|
372
|
+
resolutionOrigin,
|
|
373
|
+
ackPolicy,
|
|
283
374
|
});
|
|
284
375
|
this.hooks.audit?.({ event: "gate_response_accepted", gate_id: response.gate_id, answer_hash: answerHash });
|
|
285
|
-
await
|
|
286
|
-
this.store.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
376
|
+
await options.beforeAdvance?.();
|
|
377
|
+
await this.hooks.finalizeAccepted?.(this.store.get(response.gate_id) as PersistedGate);
|
|
378
|
+
const finalized = this.store.get(response.gate_id);
|
|
379
|
+
if (finalized?.status !== "accepted") {
|
|
380
|
+
throw new WorkflowGateBrokerError(
|
|
381
|
+
"unknown_gate",
|
|
382
|
+
`accepted gate ${response.gate_id} disappeared before advance`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (finalized.advanced) return finalized.resolution ?? resolution;
|
|
386
|
+
await this.hooks.advance?.(finalized.gate, finalized.answer);
|
|
387
|
+
const latest = this.store.get(response.gate_id);
|
|
388
|
+
if (latest?.status !== "accepted") {
|
|
389
|
+
throw new WorkflowGateBrokerError(
|
|
390
|
+
"unknown_gate",
|
|
391
|
+
`accepted gate ${response.gate_id} disappeared after advance`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
this.store.put({ ...latest, advanced: true });
|
|
295
395
|
return resolution;
|
|
296
396
|
}
|
|
297
397
|
|
|
@@ -304,15 +404,25 @@ export class WorkflowGateBroker {
|
|
|
304
404
|
async recover(): Promise<string[]> {
|
|
305
405
|
const recovered: string[] = [];
|
|
306
406
|
for (const listed of this.store.list()) {
|
|
307
|
-
if (listed.status !== "accepted" || listed.advanced) continue;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
407
|
+
if (listed.status !== "accepted" || listed.advanced || this.recovering.has(listed.gate.gate_id)) continue;
|
|
408
|
+
this.recovering.add(listed.gate.gate_id);
|
|
409
|
+
try {
|
|
410
|
+
await this.withGateLock(listed.gate.gate_id, async () => {
|
|
411
|
+
const rec = this.store.get(listed.gate.gate_id);
|
|
412
|
+
if (rec?.status !== "accepted" || rec.advanced) return;
|
|
413
|
+
await this.hooks.finalizeAccepted?.(rec);
|
|
414
|
+
const finalized = this.store.get(listed.gate.gate_id);
|
|
415
|
+
if (finalized?.status !== "accepted" || finalized.advanced) return;
|
|
416
|
+
await this.hooks.advance?.(finalized.gate, finalized.answer);
|
|
417
|
+
const latest = this.store.get(listed.gate.gate_id);
|
|
418
|
+
if (latest?.status !== "accepted" || latest.advanced) return;
|
|
419
|
+
this.store.put({ ...latest, advanced: true });
|
|
420
|
+
this.hooks.audit?.({ event: "gate_advance_recovered", gate_id: latest.gate.gate_id });
|
|
421
|
+
recovered.push(latest.gate.gate_id);
|
|
422
|
+
});
|
|
423
|
+
} finally {
|
|
424
|
+
this.recovering.delete(listed.gate.gate_id);
|
|
425
|
+
}
|
|
316
426
|
}
|
|
317
427
|
return recovered;
|
|
318
428
|
}
|