@gajae-code/coding-agent 0.11.7 → 0.11.8
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/CHANGELOG.md +55 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/config/keybindings.d.ts +33 -3
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +15 -2
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +18 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +20 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +41 -27
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +18 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +27 -2
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +835 -110
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +52 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +854 -84
- package/src/session/internal/managed-session-storage.ts +251 -199
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +310 -327
- package/src/session/session-storage.ts +128 -11
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +198 -88
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
package/src/sdk/bus/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
|
31
31
|
import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
|
|
32
32
|
import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
|
|
33
33
|
import { logger, postmortem, VERSION } from "@gajae-code/utils";
|
|
34
|
+
import { projectModelProfileCatalog } from "../../config/model-profile-contract";
|
|
34
35
|
import { Settings } from "../../config/settings";
|
|
35
36
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
36
37
|
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
@@ -59,13 +60,13 @@ import { SessionSdkHost, shouldHostSdk } from "../host";
|
|
|
59
60
|
import { type ControlSurface, dispatchControl } from "../host/control";
|
|
60
61
|
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "../host/query";
|
|
61
62
|
import { projectQ10Models } from "../models.js";
|
|
63
|
+
import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
|
|
62
64
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
63
65
|
import {
|
|
64
66
|
lifecycleStartupCapabilityForApi,
|
|
65
67
|
normalizeSdkStartupFailure,
|
|
66
68
|
type SdkStartupFailure,
|
|
67
69
|
} from "../startup-capability";
|
|
68
|
-
|
|
69
70
|
import { registerTelegramFileSink } from "./attachment-registry";
|
|
70
71
|
import { ensureDiscordDaemon, ensureSlackDaemon } from "./chat-daemon-control";
|
|
71
72
|
import {
|
|
@@ -81,6 +82,7 @@ import {
|
|
|
81
82
|
import { telegramControlCommandUsage } from "./config-commands";
|
|
82
83
|
import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
|
|
83
84
|
import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
|
|
85
|
+
import { createPromptReconciliation, sanitizePromptFailure } from "./prompt-reconciliation";
|
|
84
86
|
import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
|
|
85
87
|
import {
|
|
86
88
|
ASK_SELECTED_ACK_CAPABILITY,
|
|
@@ -401,6 +403,7 @@ interface UnattendedGatePresentation {
|
|
|
401
403
|
question: string;
|
|
402
404
|
options: string[];
|
|
403
405
|
controls: readonly AskRemoteControl[];
|
|
406
|
+
recommendedIndex?: number;
|
|
404
407
|
multi: boolean;
|
|
405
408
|
allowEmpty: boolean;
|
|
406
409
|
navigationLabel?: "Next" | "Done";
|
|
@@ -409,6 +412,14 @@ interface UnattendedGatePresentation {
|
|
|
409
412
|
onActivated?: (actionId: string, lease: { actionId: string; registrationEpoch: number }) => void;
|
|
410
413
|
onClosed?: () => void;
|
|
411
414
|
}
|
|
415
|
+
function recommendedIndexFromGateOptions(options: readonly unknown[]): number | undefined {
|
|
416
|
+
const descriptions = options.map(option => (option as { description?: unknown }).description);
|
|
417
|
+
const recommended = descriptions.filter(description => description === "recommended");
|
|
418
|
+
return recommended.length === 1 &&
|
|
419
|
+
descriptions.every(description => description === undefined || description === "recommended")
|
|
420
|
+
? descriptions.indexOf("recommended")
|
|
421
|
+
: undefined;
|
|
422
|
+
}
|
|
412
423
|
|
|
413
424
|
type RetireStatus = "retired" | "already_terminal" | "claimed" | "stale";
|
|
414
425
|
type DirectControlOutcome = "accepted" | "rejected" | "unknown";
|
|
@@ -739,6 +750,9 @@ export class PresentationArbiter {
|
|
|
739
750
|
? `(${presentation.selectedOptions.length} selected) ${presentation.question}`
|
|
740
751
|
: presentation.question,
|
|
741
752
|
options: presentation.options,
|
|
753
|
+
...(presentation.recommendedIndex === undefined
|
|
754
|
+
? {}
|
|
755
|
+
: { recommendedIndex: presentation.recommendedIndex }),
|
|
742
756
|
controls: presentation.multi
|
|
743
757
|
? [
|
|
744
758
|
{
|
|
@@ -906,6 +920,13 @@ interface SessionRuntime {
|
|
|
906
920
|
/** Records a correlated prompt terminal boundary after agent unwind. */
|
|
907
921
|
/** Atomically claims a correlated prompt terminal boundary after agent unwind. */
|
|
908
922
|
recordPromptTerminal: (correlation: { commandId: string; turnId: string } | undefined) => boolean;
|
|
923
|
+
/** Transitions the authoritative reconciliation record at lifecycle ingress; terminal outcomes settle once. */
|
|
924
|
+
notePromptReconciliation: (
|
|
925
|
+
correlation: { commandId: string; turnId: string } | undefined,
|
|
926
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
927
|
+
) => void;
|
|
928
|
+
/** Settles and emits one sanitized correlated prompt failure. */
|
|
929
|
+
emitPromptFailure: (correlation: { commandId: string; turnId: string }, error: unknown) => void;
|
|
909
930
|
/** Records correlated lifecycle frames for replay and delivers them only to the accepted requester after acknowledgement. */
|
|
910
931
|
emitPromptLifecycle: (
|
|
911
932
|
correlation: { commandId: string; turnId: string } | undefined,
|
|
@@ -939,6 +960,12 @@ interface SessionRuntime {
|
|
|
939
960
|
turnClosed?: boolean;
|
|
940
961
|
/** Finalized while provisional policy was held; flush exactly once on stable activation. */
|
|
941
962
|
pendingFinal?: { text?: string; messageRef?: string };
|
|
963
|
+
/**
|
|
964
|
+
* Lean-mode deferred final answer: latest assistant text observed at `turn_end`,
|
|
965
|
+
* emitted once at `agent_end` so intermediate tool-turn narration does not flood
|
|
966
|
+
* remote clients. Cleared after flush or when replaced by a newer turn.
|
|
967
|
+
*/
|
|
968
|
+
pendingSettled?: { text: string; messageRef?: string };
|
|
942
969
|
/** Durable gates emitted while ownership is provisional; presented only after stable activation. */
|
|
943
970
|
deferredGatePresentations: Array<() => void>;
|
|
944
971
|
/** SDK control frames received during provisional ownership; replayed only after stable activation. */
|
|
@@ -954,7 +981,6 @@ interface SessionRuntime {
|
|
|
954
981
|
const SENSITIVE_MODEL_LABEL =
|
|
955
982
|
/(?:\b(?:https?|wss?):\/\/|\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b|\b(?:api[-_ ]?key|access[-_ ]?token|bearer|secret|password|account(?:\s*id)?|email|exception|stack trace)\b|\b(?:sk|pk|rk)-[A-Za-z0-9_-]{12,}\b)/i;
|
|
956
983
|
const TOOL_SUMMARY_MAX = 280;
|
|
957
|
-
const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
|
|
958
984
|
|
|
959
985
|
/** Stable projection of the tool-owned safe-display seam (never the full Tool surface). */
|
|
960
986
|
type SafeSummaryTool = Pick<Tool, "safeSummary" | "safeSummaryFields">;
|
|
@@ -1019,6 +1045,14 @@ function pushSessionFrame(
|
|
|
1019
1045
|
runtime.server.pushFrame(JSON.stringify(frame));
|
|
1020
1046
|
}
|
|
1021
1047
|
|
|
1048
|
+
async function pushTerminalSessionFrame(
|
|
1049
|
+
runtime: Pick<SessionRuntime, "server" | "host">,
|
|
1050
|
+
frame: { type: "session_closed"; sessionId: string },
|
|
1051
|
+
): Promise<boolean> {
|
|
1052
|
+
runtime.host.emitEvent({ kind: frame.type, payload: frame });
|
|
1053
|
+
return await runtime.server.pushFrameAndWait(JSON.stringify(frame), 1_000);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1022
1056
|
function pushFileAttachment(
|
|
1023
1057
|
runtime: Pick<SessionRuntime, "server" | "host">,
|
|
1024
1058
|
frame: { type: "file_attachment"; sessionId: string; name: string; mime?: string; caption?: string },
|
|
@@ -1593,6 +1627,7 @@ function registerInteractiveAnswerSource(
|
|
|
1593
1627
|
question: request.question,
|
|
1594
1628
|
options: request.options,
|
|
1595
1629
|
controls: request.controls,
|
|
1630
|
+
recommendedIndex: request.recommendedIndex,
|
|
1596
1631
|
multi: false,
|
|
1597
1632
|
allowEmpty: false,
|
|
1598
1633
|
selectedOptions: [],
|
|
@@ -1755,6 +1790,7 @@ function sdkQuerySurface(
|
|
|
1755
1790
|
followupQueueDepth: 0,
|
|
1756
1791
|
}),
|
|
1757
1792
|
configOverrides: ReadonlyMap<string, unknown> = new Map(),
|
|
1793
|
+
promptStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown,
|
|
1758
1794
|
): SessionSurface {
|
|
1759
1795
|
const metadata = () => ({
|
|
1760
1796
|
sessionId: id,
|
|
@@ -1822,6 +1858,8 @@ function sdkQuerySurface(
|
|
|
1822
1858
|
const currentThinkingLevel = api.getThinkingLevel();
|
|
1823
1859
|
return projectQ10Models({ models, currentModel, currentThinkingLevel });
|
|
1824
1860
|
},
|
|
1861
|
+
getModelProfiles: () =>
|
|
1862
|
+
projectModelProfileCatalog(ctx.modelRegistry.getModelProfiles(), ctx.modelRegistry.getError()),
|
|
1825
1863
|
getSkillState: () => ctx.getSkillState(),
|
|
1826
1864
|
getGates: () => {
|
|
1827
1865
|
const workflowGate = ctx.workflowGate;
|
|
@@ -1861,6 +1899,8 @@ function sdkQuerySurface(
|
|
|
1861
1899
|
getExtensions: () => ctx.getExtensions(),
|
|
1862
1900
|
getArtifactRange: (id, offset, length) => ctx.getArtifactRange?.(id, offset, length),
|
|
1863
1901
|
getJobs: () => ctx.getJobs(),
|
|
1902
|
+
getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1903
|
+
promptStatusLookup(selector),
|
|
1864
1904
|
installedQueries: installedOperations(ctx, "query"),
|
|
1865
1905
|
};
|
|
1866
1906
|
}
|
|
@@ -1886,10 +1926,14 @@ function sdkControlSurface(
|
|
|
1886
1926
|
onPromptAccepted: (
|
|
1887
1927
|
correlation: { commandId: string; turnId: string },
|
|
1888
1928
|
requesterConnectionId?: string,
|
|
1929
|
+
clientRef?: string,
|
|
1930
|
+
trackReconciliation?: boolean,
|
|
1889
1931
|
) => void = () => {},
|
|
1890
1932
|
onPromptFailed: (correlation: { commandId: string; turnId: string }, error: unknown) => void = () => {},
|
|
1891
1933
|
acceptGateResolution: () => boolean,
|
|
1892
1934
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>,
|
|
1935
|
+
admitPrompt: (clientRef?: string) => void,
|
|
1936
|
+
releasePromptAdmission: (clientRef?: string) => void,
|
|
1893
1937
|
settings?: Settings,
|
|
1894
1938
|
configOverrides: Map<string, unknown> = new Map(),
|
|
1895
1939
|
configRevision: { current: number } = { current: 0 },
|
|
@@ -1966,19 +2010,35 @@ function sdkControlSurface(
|
|
|
1966
2010
|
deliverAs?: "steer" | "followUp",
|
|
1967
2011
|
rejectWhenBusy = false,
|
|
1968
2012
|
requesterConnectionId?: string,
|
|
2013
|
+
clientRef?: string,
|
|
2014
|
+
trackReconciliation = false,
|
|
1969
2015
|
) => {
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2016
|
+
const trimmedClientRef = typeof clientRef === "string" ? clientRef.trim() : undefined;
|
|
2017
|
+
if (clientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH))
|
|
2018
|
+
throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
|
|
2019
|
+
code: "invalid_input",
|
|
1973
2020
|
});
|
|
2021
|
+
if (trackReconciliation) admitPrompt(trimmedClientRef);
|
|
2022
|
+
try {
|
|
2023
|
+
if (forceFresh && isSessionBusy()) {
|
|
2024
|
+
throw Object.assign(
|
|
2025
|
+
new Error("Previous turn did not finish aborting before replacement prompt submission."),
|
|
2026
|
+
{
|
|
2027
|
+
code: "busy",
|
|
2028
|
+
},
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
if (rejectWhenBusy && isSessionBusy())
|
|
2032
|
+
throw Object.assign(
|
|
2033
|
+
new Error("turn.prompt is unavailable while the agent is busy; use turn.steer explicitly."),
|
|
2034
|
+
{
|
|
2035
|
+
code: "busy",
|
|
2036
|
+
},
|
|
2037
|
+
);
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
|
|
2040
|
+
throw error;
|
|
1974
2041
|
}
|
|
1975
|
-
if (rejectWhenBusy && isSessionBusy())
|
|
1976
|
-
throw Object.assign(
|
|
1977
|
-
new Error("turn.prompt is unavailable while the agent is busy; use turn.steer explicitly."),
|
|
1978
|
-
{
|
|
1979
|
-
code: "busy",
|
|
1980
|
-
},
|
|
1981
|
-
);
|
|
1982
2042
|
const promptImages = Array.isArray(images) ? (images as { data: string; mimeType?: string }[]) : [];
|
|
1983
2043
|
const content: string | (TextContent | ImageContent)[] =
|
|
1984
2044
|
promptImages.length > 0
|
|
@@ -2010,7 +2070,7 @@ function sdkControlSurface(
|
|
|
2010
2070
|
const onPreflightAccepted = () => {
|
|
2011
2071
|
if (preflightSettled) return;
|
|
2012
2072
|
accepted = true;
|
|
2013
|
-
onPromptAccepted(correlation, requesterConnectionId);
|
|
2073
|
+
onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
|
|
2014
2074
|
settlePreflight({ status: "accepted" });
|
|
2015
2075
|
};
|
|
2016
2076
|
// Do not acknowledge the prompt until AgentSession's async preflight
|
|
@@ -2047,13 +2107,17 @@ function sdkControlSurface(
|
|
|
2047
2107
|
try {
|
|
2048
2108
|
const result = await preflight.promise;
|
|
2049
2109
|
if (result.status === "rejected") throw result.error;
|
|
2050
|
-
return { commandId, turnId, accepted: true };
|
|
2110
|
+
return { commandId, turnId, accepted: true, ...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}) };
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
|
|
2113
|
+
throw error;
|
|
2051
2114
|
} finally {
|
|
2052
2115
|
pendingPreflightCancellations.delete(cancelPreflight);
|
|
2053
2116
|
}
|
|
2054
2117
|
};
|
|
2055
2118
|
const surface: ControlSurface & { cancelPendingPreflights(): void } = {
|
|
2056
|
-
prompt: (text, images) =>
|
|
2119
|
+
prompt: (text, images, clientRef) =>
|
|
2120
|
+
submitPrompt(text, images, false, undefined, true, controlRequesterContext.getStore(), clientRef, true),
|
|
2057
2121
|
steer: text => sendSteer(text),
|
|
2058
2122
|
followUp: text => submitPrompt(text, undefined, false, "followUp", false, controlRequesterContext.getStore()),
|
|
2059
2123
|
abort: () => {
|
|
@@ -2839,7 +2903,6 @@ export function createNotificationsExtension(
|
|
|
2839
2903
|
"session.branch",
|
|
2840
2904
|
]);
|
|
2841
2905
|
const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
|
|
2842
|
-
const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
|
2843
2906
|
|
|
2844
2907
|
async function stopSession(
|
|
2845
2908
|
id: string,
|
|
@@ -2914,6 +2977,12 @@ export function createNotificationsExtension(
|
|
|
2914
2977
|
try {
|
|
2915
2978
|
rt.workflowGate?.setRuntimeTurnProvider?.(null);
|
|
2916
2979
|
} catch {}
|
|
2980
|
+
try {
|
|
2981
|
+
const delivered = await pushTerminalSessionFrame(rt, { type: "session_closed", sessionId: id });
|
|
2982
|
+
if (!delivered) logger.warn("notifications: session_closed socket delivery was not acknowledged");
|
|
2983
|
+
} catch (e) {
|
|
2984
|
+
logger.warn(`notifications: session_closed failed: ${String(e)}`);
|
|
2985
|
+
}
|
|
2917
2986
|
await rt.waitForGateResolutionQuiescence();
|
|
2918
2987
|
try {
|
|
2919
2988
|
rt.disposeAckRecoveryParticipant();
|
|
@@ -2955,14 +3024,6 @@ export function createNotificationsExtension(
|
|
|
2955
3024
|
ownerReleaseFailures.push(e);
|
|
2956
3025
|
logger.warn(`sdk query snapshots: close failed: ${String(e)}`);
|
|
2957
3026
|
}
|
|
2958
|
-
let closeFrameSent = false;
|
|
2959
|
-
try {
|
|
2960
|
-
pushSessionFrame(rt, { type: "session_closed", sessionId: id });
|
|
2961
|
-
closeFrameSent = true;
|
|
2962
|
-
} catch (e) {
|
|
2963
|
-
logger.warn(`notifications: session_closed failed: ${String(e)}`);
|
|
2964
|
-
}
|
|
2965
|
-
if (closeFrameSent) await sleep(100);
|
|
2966
3027
|
let serverStopped = rt.serverStopped;
|
|
2967
3028
|
if (!serverStopped) {
|
|
2968
3029
|
try {
|
|
@@ -3142,11 +3203,20 @@ export function createNotificationsExtension(
|
|
|
3142
3203
|
failed: boolean;
|
|
3143
3204
|
error: unknown;
|
|
3144
3205
|
terminal: boolean;
|
|
3206
|
+
retainCorrelation: boolean;
|
|
3145
3207
|
createdAt: number;
|
|
3146
3208
|
bufferedFrames: Array<PromptLifecycleFrame | Record<string, unknown>>;
|
|
3147
3209
|
};
|
|
3148
3210
|
const promptSubmissions = new Map<string, PromptSubmission>();
|
|
3149
3211
|
const promptTerminalTombstones = new Map<string, number>();
|
|
3212
|
+
// Authoritative bounded reconciliation state for Q26 turn.prompt_status
|
|
3213
|
+
// (contract documented in ./prompt-reconciliation and ../prompt-status).
|
|
3214
|
+
// Active records never age into terminal; documented TTL/capacity
|
|
3215
|
+
// eviction is the only removal, after which lookups report `unknown`.
|
|
3216
|
+
const reconciliation = createPromptReconciliation();
|
|
3217
|
+
const admitPromptSubmission = reconciliation.admit;
|
|
3218
|
+
const notePromptReconciliationAccepted = reconciliation.noteAccepted;
|
|
3219
|
+
const lookupPromptStatus = reconciliation.lookup;
|
|
3150
3220
|
const removePendingPromptCorrelation = (correlation: { commandId: string; turnId: string }) => {
|
|
3151
3221
|
const pendingIndex = pendingPromptCorrelations.findIndex(
|
|
3152
3222
|
candidate => candidate.commandId === correlation.commandId && candidate.turnId === correlation.turnId,
|
|
@@ -3164,14 +3234,20 @@ export function createNotificationsExtension(
|
|
|
3164
3234
|
removePendingPromptCorrelation(correlation);
|
|
3165
3235
|
addTerminalTombstone(key);
|
|
3166
3236
|
};
|
|
3237
|
+
const expirePromptDelivery = (key: string, submission: PromptSubmission) => {
|
|
3238
|
+
promptSubmissions.delete(key);
|
|
3239
|
+
if (!submission.terminal && submission.retainCorrelation) return;
|
|
3240
|
+
const [commandId, turnId] = key.split(":", 2);
|
|
3241
|
+
if (!commandId || !turnId) return;
|
|
3242
|
+
removePendingPromptCorrelation({ commandId, turnId });
|
|
3243
|
+
addTerminalTombstone(key);
|
|
3244
|
+
};
|
|
3167
3245
|
const cleanupPromptRecords = (now = Date.now()) => {
|
|
3246
|
+
reconciliation.cleanup();
|
|
3168
3247
|
for (const [key, expiresAt] of promptTerminalTombstones)
|
|
3169
3248
|
if (expiresAt <= now) promptTerminalTombstones.delete(key);
|
|
3170
3249
|
for (const [key, submission] of promptSubmissions)
|
|
3171
|
-
if (submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now)
|
|
3172
|
-
const [commandId, turnId] = key.split(":", 2);
|
|
3173
|
-
if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
|
|
3174
|
-
}
|
|
3250
|
+
if (submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now) expirePromptDelivery(key, submission);
|
|
3175
3251
|
};
|
|
3176
3252
|
const abandonPrompt = (submission: PromptSubmission) => {
|
|
3177
3253
|
submission.abandoned = true;
|
|
@@ -3182,6 +3258,7 @@ export function createNotificationsExtension(
|
|
|
3182
3258
|
frame: PromptLifecycleFrame,
|
|
3183
3259
|
) => {
|
|
3184
3260
|
cleanupPromptRecords();
|
|
3261
|
+
|
|
3185
3262
|
if (!correlation || !runtime) {
|
|
3186
3263
|
emitAgentLifecycle(runtime!, frame as Extract<PromptLifecycleFrame, { type: "agent_start" | "agent_end" }>);
|
|
3187
3264
|
return;
|
|
@@ -3247,15 +3324,21 @@ export function createNotificationsExtension(
|
|
|
3247
3324
|
const recordPromptAccepted = (
|
|
3248
3325
|
correlation: { commandId: string; turnId: string },
|
|
3249
3326
|
requesterConnectionId?: string,
|
|
3327
|
+
clientRef?: string,
|
|
3328
|
+
trackReconciliation = false,
|
|
3250
3329
|
) => {
|
|
3251
|
-
if (!requesterConnectionId)
|
|
3330
|
+
if (!requesterConnectionId) {
|
|
3331
|
+
// No delivery owner: tracked prompts cannot be reconciled. Release
|
|
3332
|
+
// their admission reservation instead of leaking the active slot.
|
|
3333
|
+
if (trackReconciliation) reconciliation.releaseAdmission(clientRef);
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
if (trackReconciliation) notePromptReconciliationAccepted(correlation, clientRef);
|
|
3252
3337
|
cleanupPromptRecords();
|
|
3253
3338
|
while (promptSubmissions.size >= PROMPT_SUBMISSION_CAPACITY) {
|
|
3254
3339
|
const oldest = promptSubmissions.entries().next().value as [string, PromptSubmission] | undefined;
|
|
3255
3340
|
if (!oldest) break;
|
|
3256
|
-
|
|
3257
|
-
const [commandId, turnId] = key.split(":", 2);
|
|
3258
|
-
if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
|
|
3341
|
+
expirePromptDelivery(oldest[0], oldest[1]);
|
|
3259
3342
|
}
|
|
3260
3343
|
pendingPromptCorrelations.push(correlation);
|
|
3261
3344
|
promptSubmissions.set(promptSubmissionKey(correlation), {
|
|
@@ -3265,6 +3348,7 @@ export function createNotificationsExtension(
|
|
|
3265
3348
|
failed: false,
|
|
3266
3349
|
error: undefined,
|
|
3267
3350
|
terminal: false,
|
|
3351
|
+
retainCorrelation: trackReconciliation,
|
|
3268
3352
|
createdAt: Date.now(),
|
|
3269
3353
|
bufferedFrames: [],
|
|
3270
3354
|
});
|
|
@@ -3280,17 +3364,15 @@ export function createNotificationsExtension(
|
|
|
3280
3364
|
return true;
|
|
3281
3365
|
};
|
|
3282
3366
|
const emitPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
3367
|
+
const sanitized = sanitizePromptFailure(error);
|
|
3368
|
+
reconciliation.noteTransition(correlation, { type: "agent_failed", error: sanitized });
|
|
3283
3369
|
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
3284
3370
|
if (!submission || !runtime || !recordPromptTerminal(correlation)) return;
|
|
3285
|
-
const candidate = error as { code?: unknown; message?: unknown };
|
|
3286
3371
|
emitPromptLifecycle(correlation, {
|
|
3287
3372
|
type: "agent_failed",
|
|
3288
3373
|
sessionId: runtime.id,
|
|
3289
3374
|
...correlation,
|
|
3290
|
-
error:
|
|
3291
|
-
code: typeof candidate.code === "string" ? candidate.code : "internal",
|
|
3292
|
-
message: typeof candidate.message === "string" ? candidate.message : "Prompt submission failed.",
|
|
3293
|
-
},
|
|
3375
|
+
error: sanitized,
|
|
3294
3376
|
});
|
|
3295
3377
|
};
|
|
3296
3378
|
const recordPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
@@ -3333,6 +3415,7 @@ export function createNotificationsExtension(
|
|
|
3333
3415
|
};
|
|
3334
3416
|
},
|
|
3335
3417
|
configOverrides,
|
|
3418
|
+
lookupPromptStatus,
|
|
3336
3419
|
),
|
|
3337
3420
|
id,
|
|
3338
3421
|
revisions,
|
|
@@ -3348,6 +3431,8 @@ export function createNotificationsExtension(
|
|
|
3348
3431
|
recordPromptFailure,
|
|
3349
3432
|
() => runtime?.stopping !== true,
|
|
3350
3433
|
trackGateResolution,
|
|
3434
|
+
admitPromptSubmission,
|
|
3435
|
+
reconciliation.releaseAdmission,
|
|
3351
3436
|
settings,
|
|
3352
3437
|
configOverrides,
|
|
3353
3438
|
configRevision,
|
|
@@ -3406,7 +3491,7 @@ export function createNotificationsExtension(
|
|
|
3406
3491
|
stateRoot,
|
|
3407
3492
|
token,
|
|
3408
3493
|
sendFrame: (connectionId, frame) => sendSdkFrame(connectionId, frame),
|
|
3409
|
-
connectionCapabilities: connectionId => hostCapCache.get(connectionId)
|
|
3494
|
+
connectionCapabilities: connectionId => hostCapCache.get(connectionId),
|
|
3410
3495
|
installProviderDefinitions,
|
|
3411
3496
|
onProviderDefinitionsRemoved: removeProviderDefinitions,
|
|
3412
3497
|
onFrame: handler => {
|
|
@@ -3478,6 +3563,7 @@ export function createNotificationsExtension(
|
|
|
3478
3563
|
},
|
|
3479
3564
|
),
|
|
3480
3565
|
);
|
|
3566
|
+
|
|
3481
3567
|
if (rotatesIdentity && response.ok !== true) {
|
|
3482
3568
|
identityControlInFlight = false;
|
|
3483
3569
|
deferredIdentityRotation = undefined;
|
|
@@ -3547,6 +3633,8 @@ export function createNotificationsExtension(
|
|
|
3547
3633
|
pendingPromptCorrelations,
|
|
3548
3634
|
activePromptCorrelation: undefined,
|
|
3549
3635
|
recordPromptTerminal,
|
|
3636
|
+
notePromptReconciliation: reconciliation.noteTransition,
|
|
3637
|
+
emitPromptFailure,
|
|
3550
3638
|
emitPromptLifecycle,
|
|
3551
3639
|
emitPromptEvent,
|
|
3552
3640
|
pendingInbound: new Set<number>(),
|
|
@@ -3608,6 +3696,13 @@ export function createNotificationsExtension(
|
|
|
3608
3696
|
if (!frame || typeof frame !== "object") return;
|
|
3609
3697
|
const typedFrame = frame as Record<string, unknown>;
|
|
3610
3698
|
if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
|
|
3699
|
+
if (typedFrame.type === "event_replay") {
|
|
3700
|
+
const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
|
|
3701
|
+
hostCapCache.set(
|
|
3702
|
+
inbound.connectionId,
|
|
3703
|
+
new Set(capabilities.filter((capability): capability is string => typeof capability === "string")),
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3611
3706
|
inboundSdkFrame?.(inbound.connectionId, typedFrame);
|
|
3612
3707
|
} catch {}
|
|
3613
3708
|
});
|
|
@@ -3748,7 +3843,7 @@ export function createNotificationsExtension(
|
|
|
3748
3843
|
}
|
|
3749
3844
|
const gate = runtime?.workflowGate;
|
|
3750
3845
|
const workflowGateActive =
|
|
3751
|
-
gate?.
|
|
3846
|
+
gate?.supportsRemoteGateAnswers?.() === true &&
|
|
3752
3847
|
typeof gate.onGateEmitted === "function" &&
|
|
3753
3848
|
typeof gate.resolveGateFromNotification === "function";
|
|
3754
3849
|
const gateId = gatePresentations.routeFor(reply.id);
|
|
@@ -4201,7 +4296,9 @@ export function createNotificationsExtension(
|
|
|
4201
4296
|
? Gate
|
|
4202
4297
|
: never,
|
|
4203
4298
|
): void => {
|
|
4204
|
-
const
|
|
4299
|
+
const rawGateOptions = g.options ?? [];
|
|
4300
|
+
const options = rawGateOptions.map(o => String((o as { label?: unknown }).label ?? ""));
|
|
4301
|
+
const recommendedIndex = recommendedIndexFromGateOptions(rawGateOptions);
|
|
4205
4302
|
gateOptions.set(g.gate_id, options);
|
|
4206
4303
|
const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
|
|
4207
4304
|
const question =
|
|
@@ -4218,6 +4315,7 @@ export function createNotificationsExtension(
|
|
|
4218
4315
|
sessionId: id,
|
|
4219
4316
|
question,
|
|
4220
4317
|
options,
|
|
4318
|
+
...(recommendedIndex === undefined ? {} : { recommendedIndex }),
|
|
4221
4319
|
controls: [],
|
|
4222
4320
|
multi: stageState.multi === true,
|
|
4223
4321
|
allowEmpty: stageState.allow_empty === true,
|
|
@@ -4576,22 +4674,45 @@ export function createNotificationsExtension(
|
|
|
4576
4674
|
if (!pending) return;
|
|
4577
4675
|
rt.pendingFinal = undefined;
|
|
4578
4676
|
if (pending.text && rt.notificationsActive && !rt.redact) {
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
phase: "finalized",
|
|
4584
|
-
finalAnswer: true,
|
|
4677
|
+
// Under lean, hold intermediate finals until agent_end when the agent is still
|
|
4678
|
+
// running so provisional-policy activation cannot reintroduce per-turn spam.
|
|
4679
|
+
if (rt.verbosity === "lean" && rt.busy) {
|
|
4680
|
+
rt.pendingSettled = {
|
|
4585
4681
|
text: pending.text,
|
|
4586
4682
|
...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
|
|
4587
|
-
}
|
|
4588
|
-
}
|
|
4589
|
-
|
|
4683
|
+
};
|
|
4684
|
+
} else {
|
|
4685
|
+
try {
|
|
4686
|
+
pushSessionFrame(rt, {
|
|
4687
|
+
type: "turn_stream",
|
|
4688
|
+
sessionId: id,
|
|
4689
|
+
phase: "finalized",
|
|
4690
|
+
finalAnswer: true,
|
|
4691
|
+
text: pending.text,
|
|
4692
|
+
...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
|
|
4693
|
+
});
|
|
4694
|
+
} catch (error) {
|
|
4695
|
+
logger.warn(`notifications: pushFrame (pending turn) failed: ${String(error)}`);
|
|
4696
|
+
}
|
|
4590
4697
|
}
|
|
4591
4698
|
}
|
|
4592
4699
|
resetTurnStreamState(rt);
|
|
4593
4700
|
};
|
|
4594
4701
|
|
|
4702
|
+
/** Emit the deferred lean settled answer exactly once (agent_end / idle). */
|
|
4703
|
+
const flushPendingSettled = (rt: SessionRuntime, id: string): void => {
|
|
4704
|
+
const settled = rt.pendingSettled;
|
|
4705
|
+
rt.pendingSettled = undefined;
|
|
4706
|
+
if (!settled?.text || !rt.notificationsActive || rt.redact || rt.policySuspended) return;
|
|
4707
|
+
const previousLiveRef = rt.liveRef;
|
|
4708
|
+
if (settled.messageRef) rt.liveRef = settled.messageRef;
|
|
4709
|
+
try {
|
|
4710
|
+
flushTurnText(rt, id, settled.text, true);
|
|
4711
|
+
} finally {
|
|
4712
|
+
if (!settled.messageRef) rt.liveRef = previousLiveRef;
|
|
4713
|
+
}
|
|
4714
|
+
};
|
|
4715
|
+
|
|
4595
4716
|
// Drive the live typing indicator: mark busy when the agent loop starts so
|
|
4596
4717
|
// the daemon shows "typing…" in the thread while the agent is thinking,
|
|
4597
4718
|
// before any turn output exists. Cleared on `agent_end` below.
|
|
@@ -4604,6 +4725,7 @@ export function createNotificationsExtension(
|
|
|
4604
4725
|
rt.busy = true;
|
|
4605
4726
|
const correlation = rt.pendingPromptCorrelations.shift();
|
|
4606
4727
|
rt.activePromptCorrelation = correlation;
|
|
4728
|
+
rt.notePromptReconciliation(correlation, { type: "agent_start" });
|
|
4607
4729
|
rt.emitPromptLifecycle(correlation, { type: "agent_start", sessionId: id, ...correlation });
|
|
4608
4730
|
try {
|
|
4609
4731
|
// `activity` is the native live-host lifecycle surface. The separately
|
|
@@ -4648,8 +4770,35 @@ export function createNotificationsExtension(
|
|
|
4648
4770
|
rt.busy = false;
|
|
4649
4771
|
const correlation = rt.activePromptCorrelation;
|
|
4650
4772
|
if (correlation) {
|
|
4651
|
-
|
|
4652
|
-
|
|
4773
|
+
const terminalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
|
|
4774
|
+
message =>
|
|
4775
|
+
message &&
|
|
4776
|
+
typeof message === "object" &&
|
|
4777
|
+
(message as { role?: unknown }).role === "assistant" &&
|
|
4778
|
+
((message as { stopReason?: unknown }).stopReason === "error" ||
|
|
4779
|
+
(message as { stopReason?: unknown }).stopReason === "aborted"),
|
|
4780
|
+
) as { stopReason?: "error" | "aborted"; errorMessage?: unknown } | undefined;
|
|
4781
|
+
const failureCode =
|
|
4782
|
+
event.stopReason === "cancelled"
|
|
4783
|
+
? "cancelled"
|
|
4784
|
+
: terminalAssistant?.stopReason === "error"
|
|
4785
|
+
? "agent_error"
|
|
4786
|
+
: terminalAssistant?.stopReason === "aborted"
|
|
4787
|
+
? "aborted"
|
|
4788
|
+
: undefined;
|
|
4789
|
+
if (failureCode)
|
|
4790
|
+
rt.emitPromptFailure(correlation, {
|
|
4791
|
+
code: failureCode,
|
|
4792
|
+
message:
|
|
4793
|
+
typeof terminalAssistant?.errorMessage === "string"
|
|
4794
|
+
? terminalAssistant.errorMessage
|
|
4795
|
+
: "Prompt submission failed.",
|
|
4796
|
+
});
|
|
4797
|
+
else {
|
|
4798
|
+
rt.notePromptReconciliation(correlation, { type: "agent_end" });
|
|
4799
|
+
if (rt.recordPromptTerminal(correlation))
|
|
4800
|
+
rt.emitPromptLifecycle(correlation, { type: "agent_end", sessionId: id, ...correlation });
|
|
4801
|
+
}
|
|
4653
4802
|
} else {
|
|
4654
4803
|
rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
|
|
4655
4804
|
}
|
|
@@ -4693,9 +4842,14 @@ export function createNotificationsExtension(
|
|
|
4693
4842
|
logger.warn(`notifications: noteIdle failed: ${String(e)}`);
|
|
4694
4843
|
}
|
|
4695
4844
|
|
|
4845
|
+
// Lean: emit the latest deferred assistant answer exactly once at idle.
|
|
4846
|
+
// Intermediate tool-turn narration was held on turn_end; ask lead-ins were
|
|
4847
|
+
// already flushed immediately and deduped via preAskFlushedText.
|
|
4848
|
+
flushPendingSettled(rt, id);
|
|
4849
|
+
|
|
4696
4850
|
// On idle, stream a context update with metadata (token/model usage +
|
|
4697
|
-
// working-tree diff) unless redaction is on.
|
|
4698
|
-
//
|
|
4851
|
+
// working-tree diff) unless redaction is on. Under verbose the agent's last
|
|
4852
|
+
// message is already streamed per turn_end; lean flushes it just above.
|
|
4699
4853
|
if (!rt.redact && rt.verbosity === "verbose") {
|
|
4700
4854
|
const usage = (
|
|
4701
4855
|
ctx as { getContextUsage?: () => { tokens: number | null; contextWindow: number } | undefined }
|
|
@@ -4724,17 +4878,20 @@ export function createNotificationsExtension(
|
|
|
4724
4878
|
}
|
|
4725
4879
|
});
|
|
4726
4880
|
|
|
4727
|
-
// Stream viable agent output
|
|
4728
|
-
//
|
|
4729
|
-
//
|
|
4730
|
-
//
|
|
4731
|
-
//
|
|
4732
|
-
// rate-limit pool
|
|
4881
|
+
// Stream viable agent output. Verbose mirrors each turn that produced assistant
|
|
4882
|
+
// text. Lean defers the latest answer until agent_end (idle) so tool-heavy runs
|
|
4883
|
+
// do not flood remote clients with intermediate narration; ask lead-ins still
|
|
4884
|
+
// flush immediately. Tool-only turns yield no text and are skipped. Redaction
|
|
4885
|
+
// suppresses streamed content (only the one-time identity header survives).
|
|
4886
|
+
// The daemon coalesces/throttles these via its shared rate-limit pool.
|
|
4733
4887
|
// Push the in-flight turn's assistant text as a finalized turn_stream, deduped
|
|
4734
4888
|
// against what was already flushed for this turn (the pre-ask lead-in).
|
|
4735
4889
|
const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined, finalAnswer: boolean): void => {
|
|
4736
4890
|
if (!text || text === rt.preAskFlushedText || !rt.notificationsActive || rt.policySuspended) return;
|
|
4737
4891
|
rt.preAskFlushedText = text;
|
|
4892
|
+
// Ask lead-ins supersede any deferred lean settled answer from earlier turns
|
|
4893
|
+
// so agent_end does not re-emit stale intermediate narration (#2863 review).
|
|
4894
|
+
if (!finalAnswer) rt.pendingSettled = undefined;
|
|
4738
4895
|
// Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
|
|
4739
4896
|
// live message, never a fresh (rich-promotable) send. If live frames were
|
|
4740
4897
|
// async-queued and none landed before this flush, reuse the per-turn ref
|
|
@@ -4875,19 +5032,37 @@ export function createNotificationsExtension(
|
|
|
4875
5032
|
rt.turnClosed = true;
|
|
4876
5033
|
return;
|
|
4877
5034
|
}
|
|
4878
|
-
if (text)
|
|
5035
|
+
if (text) {
|
|
5036
|
+
if (rt.verbosity === "verbose") {
|
|
5037
|
+
// Verbose: one finalized turn_stream per turn with assistant text.
|
|
5038
|
+
flushTurnText(rt, id, text, true);
|
|
5039
|
+
} else if (text !== rt.preAskFlushedText) {
|
|
5040
|
+
// Lean: hold the latest answer until agent_end. Skip when this turn
|
|
5041
|
+
// already flushed the same text as an ask lead-in (no duplicate at idle).
|
|
5042
|
+
rt.pendingSettled = {
|
|
5043
|
+
text,
|
|
5044
|
+
...(rt.liveRef ? { messageRef: rt.liveRef } : {}),
|
|
5045
|
+
};
|
|
5046
|
+
} else {
|
|
5047
|
+
// Lead-in already flushed: drop any older deferred settled text so idle
|
|
5048
|
+
// does not re-emit intermediate narration after the ask prompt (#2863).
|
|
5049
|
+
rt.pendingSettled = undefined;
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
4879
5052
|
resetTurnStreamState(rt);
|
|
4880
5053
|
});
|
|
4881
5054
|
|
|
4882
|
-
// Live streaming (opt-in): push throttled in-progress assistant
|
|
4883
|
-
// non-finalized turn_stream frames so remote clients edit one message
|
|
4884
|
-
// turn streams.
|
|
4885
|
-
//
|
|
5055
|
+
// Live streaming (opt-in + verbose only): push throttled in-progress assistant
|
|
5056
|
+
// text as non-finalized turn_stream frames so remote clients edit one message
|
|
5057
|
+
// as the turn streams. Lean keeps settled-answer-only delivery; live frames
|
|
5058
|
+
// are suppressed even when the stream preference is on. The finalized frame
|
|
5059
|
+
// (turn_end / agent_end) carries the same messageRef and lands the authoritative
|
|
5060
|
+
// text. Suppressed under redaction.
|
|
4886
5061
|
api.on("message_update", (event, ctx) => {
|
|
4887
5062
|
const id = sessionId(ctx);
|
|
4888
5063
|
const rt = runtimes.get(id);
|
|
4889
5064
|
rt?.emitPromptEvent(event);
|
|
4890
|
-
if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
|
|
5065
|
+
if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed || rt.verbosity !== "verbose") return;
|
|
4891
5066
|
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
4892
5067
|
if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
|
|
4893
5068
|
rt.liveRef = String(rt.turnSeq);
|