@sayknow-cli/coding-agent 0.3.7 → 0.3.9
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/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +30 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +8 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +12 -3
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon.d.ts +45 -3
- package/dist/types/notifications/telegram-reference.d.ts +7 -5
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +61 -6
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/skc-runtime/state-writer.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +8 -8
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +61 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +103 -17
- package/src/cli/telegram-cli.ts +1 -1
- package/src/cli/update-cli.ts +148 -33
- package/src/cli.ts +29 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-profiles.ts +31 -2
- package/src/config/settings-schema.ts +76 -1
- package/src/config/settings.ts +22 -0
- package/src/defaults/skc/skills/team/SKILL.md +17 -14
- package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/skc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +183 -219
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +124 -4
- package/src/internal-urls/docs-index.generated.ts +17 -8
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +95 -43
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +181 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +12 -11
- package/src/modes/controllers/input-controller.ts +192 -35
- package/src/modes/controllers/selector-controller.ts +168 -24
- package/src/modes/interactive-mode.ts +56 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +20 -7
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +52 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +144 -18
- package/src/notifications/lifecycle-commands.ts +74 -23
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +606 -98
- package/src/notifications/telegram-reference.ts +17 -16
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +29 -17
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/computer.md +2 -2
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +88 -36
- package/src/session/agent-session.ts +1013 -141
- package/src/session/agent-storage.ts +4 -0
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +103 -4
- package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/skc-runtime/deep-interview-recorder.ts +62 -8
- package/src/skc-runtime/deep-interview-state.ts +44 -2
- package/src/skc-runtime/session-state-sidecar.ts +263 -20
- package/src/skc-runtime/state-runtime.ts +12 -2
- package/src/skc-runtime/state-writer.ts +15 -0
- package/src/skc-runtime/tmux-common.ts +1 -0
- package/src/skc-runtime/ultragoal-guard.ts +55 -66
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/skc-runtime/ultragoal-runtime.ts +1672 -159
- package/src/skc-runtime/workflow-manifest.generated.json +113 -2
- package/src/skc-runtime/workflow-manifest.ts +74 -1
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +296 -72
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +132 -24
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +13 -6
- package/src/task/output-manager.ts +20 -8
- package/src/task/receipt.ts +26 -7
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/computer.ts +246 -43
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/tools/telegram-send.ts +6 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "@sayknow-cli/ai";
|
|
2
|
+
import type { InteractiveModeContext } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalize the content passed to an extension `sendUserMessage` call into a
|
|
6
|
+
* plain text string plus its image attachments. Mirrors the normalization in
|
|
7
|
+
* `AgentSession.sendUserMessage` (text parts joined with "\n") so the resulting
|
|
8
|
+
* text matches the eventual user `message_start` payload.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeInjectedUserContent(content: string | (TextContent | ImageContent)[]): {
|
|
11
|
+
text: string;
|
|
12
|
+
images: ImageContent[];
|
|
13
|
+
imageCount: number;
|
|
14
|
+
} {
|
|
15
|
+
if (typeof content === "string") {
|
|
16
|
+
return { text: content, images: [], imageCount: 0 };
|
|
17
|
+
}
|
|
18
|
+
const textParts: string[] = [];
|
|
19
|
+
const images: ImageContent[] = [];
|
|
20
|
+
for (const part of content) {
|
|
21
|
+
if (part.type === "text") textParts.push(part.text);
|
|
22
|
+
else images.push(part);
|
|
23
|
+
}
|
|
24
|
+
const text = textParts.join("\n");
|
|
25
|
+
return { text, images, imageCount: images.length };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Record a remotely/programmatically injected user message (e.g. Telegram
|
|
30
|
+
* inbound routed through the extension API) into the interactive TUI, so it is
|
|
31
|
+
* captured in prompt history and shown immediately instead of only appearing
|
|
32
|
+
* once the eventual `message_start` event lands.
|
|
33
|
+
*
|
|
34
|
+
* Local TUI submissions never reach this path (they go through
|
|
35
|
+
* `session.prompt(...)` / `startPendingSubmission`), so this cannot double-add
|
|
36
|
+
* local prompt history.
|
|
37
|
+
*
|
|
38
|
+
* - Always adds the injected text to editor prompt history.
|
|
39
|
+
* - Idle injections optimistically render the user message and record a pending
|
|
40
|
+
* injected optimistic signature (a counting Map, so multiple idle injections
|
|
41
|
+
* before the first `message_start` do not clobber each other); the later user
|
|
42
|
+
* `message_start` consumes one count and skips both the duplicate chat add and
|
|
43
|
+
* the defensive editor clear (so a locally typed draft is preserved).
|
|
44
|
+
* - Busy/queued injections refresh the pending-message display, which the
|
|
45
|
+
* caller has already populated by invoking `session.sendUserMessage(...)`
|
|
46
|
+
* before this helper.
|
|
47
|
+
*
|
|
48
|
+
* This helper never clears the editor text.
|
|
49
|
+
*/
|
|
50
|
+
export function applyInjectedUserSubmission(
|
|
51
|
+
ctx: InteractiveModeContext,
|
|
52
|
+
input: { content: string | (TextContent | ImageContent)[]; queued: boolean },
|
|
53
|
+
): void {
|
|
54
|
+
const { text, images, imageCount } = normalizeInjectedUserContent(input.content);
|
|
55
|
+
ctx.editor.addToHistory(text);
|
|
56
|
+
|
|
57
|
+
if (input.queued) {
|
|
58
|
+
ctx.updatePendingMessagesDisplay();
|
|
59
|
+
ctx.ui.requestRender();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
incrementInjectedOptimisticSignature(ctx, `${text}\u0000${imageCount}`);
|
|
64
|
+
ctx.addMessageToChat({
|
|
65
|
+
role: "user",
|
|
66
|
+
content: [{ type: "text", text }, ...images],
|
|
67
|
+
attribution: "user",
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
});
|
|
70
|
+
ctx.ui.requestRender();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Record one pending optimistic render for an injected user message.
|
|
75
|
+
*
|
|
76
|
+
* Injected sends are fire-and-forget and multiple idle injections can be
|
|
77
|
+
* rendered before the first `message_start` arrives, so a counting Map (not a
|
|
78
|
+
* single slot) tracks how many optimistic renders are outstanding per signature.
|
|
79
|
+
*/
|
|
80
|
+
export function incrementInjectedOptimisticSignature(ctx: InteractiveModeContext, signature: string): void {
|
|
81
|
+
ctx.optimisticInjectedSignatures.set(signature, (ctx.optimisticInjectedSignatures.get(signature) ?? 0) + 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Consume one pending injected optimistic render for `signature`. Decrements the
|
|
86
|
+
* count (deleting the key at zero) and returns whether one was outstanding.
|
|
87
|
+
*/
|
|
88
|
+
export function consumeInjectedOptimisticSignature(ctx: InteractiveModeContext, signature: string): boolean {
|
|
89
|
+
const count = ctx.optimisticInjectedSignatures.get(signature) ?? 0;
|
|
90
|
+
if (count <= 0) return false;
|
|
91
|
+
if (count === 1) ctx.optimisticInjectedSignatures.delete(signature);
|
|
92
|
+
else ctx.optimisticInjectedSignatures.set(signature, count - 1);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
@@ -2,9 +2,9 @@ import type { AgentMessage } from "@sayknow-cli/agent-core";
|
|
|
2
2
|
import type { AssistantMessage, ImageContent, Message } from "@sayknow-cli/ai";
|
|
3
3
|
import { type Component, Spacer, Text, TruncatedText } from "@sayknow-cli/tui";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
|
-
import { tMessage } from "../../i18n";
|
|
6
5
|
import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/skc-plugins";
|
|
7
6
|
import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
|
|
7
|
+
import { tMessage } from "../../i18n";
|
|
8
8
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
9
9
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
10
10
|
import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
|
|
@@ -37,6 +37,19 @@ interface RenderInitialMessagesOptions {
|
|
|
37
37
|
preserveExistingChat?: boolean;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function argsWithPartialJson(args: unknown, partialJson: unknown): unknown {
|
|
41
|
+
if (typeof partialJson !== "string" || !args || typeof args !== "object" || Array.isArray(args)) return args;
|
|
42
|
+
// Non-enumerable so the transient streaming buffer reaches renderers via direct
|
|
43
|
+
// property read but never serializes into persisted/wire message content.
|
|
44
|
+
Object.defineProperty(args, "__partialJson", {
|
|
45
|
+
value: partialJson,
|
|
46
|
+
enumerable: false,
|
|
47
|
+
configurable: true,
|
|
48
|
+
writable: true,
|
|
49
|
+
});
|
|
50
|
+
return args;
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
type QueuedMessages = {
|
|
41
54
|
steering: string[];
|
|
42
55
|
followUp: string[];
|
|
@@ -394,10 +407,10 @@ export class UiHelpers {
|
|
|
394
407
|
|
|
395
408
|
readGroup = null;
|
|
396
409
|
const tool = this.ctx.session.getToolByName(content.name);
|
|
397
|
-
const renderArgs =
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
410
|
+
const renderArgs = argsWithPartialJson(
|
|
411
|
+
content.arguments,
|
|
412
|
+
"partialJson" in content ? content.partialJson : undefined,
|
|
413
|
+
);
|
|
401
414
|
const component = new ToolExecutionComponent(
|
|
402
415
|
content.name,
|
|
403
416
|
renderArgs,
|
|
@@ -605,8 +618,8 @@ export class UiHelpers {
|
|
|
605
618
|
const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
|
|
606
619
|
this.ctx.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
|
607
620
|
}
|
|
608
|
-
const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue") || "Alt+Up";
|
|
609
|
-
const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to edit`);
|
|
621
|
+
const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue") || "Alt+Up/Alt+Down";
|
|
622
|
+
const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to select/edit/reorder`);
|
|
610
623
|
this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
|
|
611
624
|
}
|
|
612
625
|
}
|
|
@@ -48,3 +48,23 @@ export function parseInThreadConfigCommand(text: string): ConfigCommandChange |
|
|
|
48
48
|
return undefined;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse a `/rich on|off` toggle. Returns `true`/`false` for a recognised
|
|
54
|
+
* on/off argument, or `undefined` otherwise (not a `/rich` command, or `/rich`
|
|
55
|
+
* with a missing/invalid argument). This is intentionally SEPARATE from
|
|
56
|
+
* `parseInThreadConfigCommand`: `/verbose`/`/redact` are producer/session config
|
|
57
|
+
* forwarded over the WS, whereas rich is Telegram-daemon delivery policy handled
|
|
58
|
+
* daemon-locally, so it never becomes a `config_command` frame or a user turn.
|
|
59
|
+
*/
|
|
60
|
+
export function parseRichToggleCommand(text: string): boolean | undefined {
|
|
61
|
+
const trimmed = text.trim();
|
|
62
|
+
if (!trimmed.startsWith("/")) return undefined;
|
|
63
|
+
const [rawCommand, ...rest] = trimmed.slice(1).split(/\s+/);
|
|
64
|
+
// Accept the "/rich@botname" form Telegram appends in group chats.
|
|
65
|
+
if (rawCommand?.toLowerCase().split("@")[0] !== "rich") return undefined;
|
|
66
|
+
const arg = rest[0]?.toLowerCase();
|
|
67
|
+
if (arg === "on" || arg === "true" || arg === "1") return true;
|
|
68
|
+
if (arg === "off" || arg === "false" || arg === "0") return false;
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
@@ -16,6 +16,12 @@ export interface NotificationConfig {
|
|
|
16
16
|
redact: boolean;
|
|
17
17
|
verbosity: "lean" | "verbose";
|
|
18
18
|
idleTimeoutMs: number;
|
|
19
|
+
rich: {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
};
|
|
22
|
+
richDraft: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
};
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/** Read typed config from Settings. */
|
|
@@ -35,24 +41,63 @@ export function getNotificationConfig(settings: Settings): NotificationConfig {
|
|
|
35
41
|
redact: settings.get("notifications.redact"),
|
|
36
42
|
verbosity: settings.get("notifications.verbosity") === "verbose" ? "verbose" : "lean",
|
|
37
43
|
idleTimeoutMs: settings.get("notifications.daemon.idleTimeoutMs"),
|
|
44
|
+
rich: {
|
|
45
|
+
enabled: settings.get("notifications.telegram.rich.enabled"),
|
|
46
|
+
},
|
|
47
|
+
richDraft: {
|
|
48
|
+
enabled: settings.get("notifications.telegram.richDraft.enabled"),
|
|
49
|
+
},
|
|
38
50
|
};
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
export function hasNonBlankValue(value: string | undefined): boolean {
|
|
54
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Is Telegram configured with usable non-blank boundary credentials? */
|
|
58
|
+
export function isTelegramConfigured(
|
|
59
|
+
cfg: NotificationConfig,
|
|
60
|
+
): cfg is NotificationConfig & { botToken: string; chatId: string } {
|
|
61
|
+
return cfg.enabled && hasNonBlankValue(cfg.botToken) && hasNonBlankValue(cfg.chatId);
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
/** Is global config sufficient for auto-on (enabled + at least one configured adapter)? */
|
|
42
65
|
export function isGloballyConfigured(cfg: NotificationConfig): boolean {
|
|
43
66
|
return (
|
|
44
67
|
cfg.enabled &&
|
|
45
|
-
((
|
|
46
|
-
(
|
|
47
|
-
(
|
|
68
|
+
(isTelegramConfigured(cfg) ||
|
|
69
|
+
(hasNonBlankValue(cfg.discord.botToken) && hasNonBlankValue(cfg.discord.channelId)) ||
|
|
70
|
+
(hasNonBlankValue(cfg.slack.botToken) && hasNonBlankValue(cfg.slack.channelId)))
|
|
48
71
|
);
|
|
49
72
|
}
|
|
50
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Per-run opt-out for completion notifications, honored before settings lookups.
|
|
76
|
+
*
|
|
77
|
+
* `SKC_NOTIFY=off` (also `0` / `false`, case-insensitive) suppresses the
|
|
78
|
+
* completion notification surface for this process only. `config.yml` is
|
|
79
|
+
* untouched and child processes inherit the env var, which lets non-interactive
|
|
80
|
+
* fleet runs (`skc -p --no-session`) stay silent even when a user-level/global
|
|
81
|
+
* completion notification configuration is enabled.
|
|
82
|
+
*/
|
|
83
|
+
export function completionNotifyDisabledByEnv(env: NodeJS.ProcessEnv): boolean {
|
|
84
|
+
const v = env.SKC_NOTIFY?.trim().toLowerCase();
|
|
85
|
+
return v === "off" || v === "0" || v === "false";
|
|
86
|
+
}
|
|
87
|
+
|
|
51
88
|
/** Resolve whether the notifications extension should be registered at SDK startup. */
|
|
52
89
|
export function shouldRegisterNotificationsExtension(input: {
|
|
53
90
|
env: NodeJS.ProcessEnv;
|
|
54
91
|
cfg?: NotificationConfig;
|
|
92
|
+
/** Task recursion depth; helper/subagent sessions must not spawn remote surfaces. */
|
|
93
|
+
taskDepth?: number;
|
|
94
|
+
/** Parent subagent id/prefix; present for helper/subagent sessions even when depth is omitted. */
|
|
95
|
+
parentTaskPrefix?: string;
|
|
96
|
+
/** Role-agent type/name; present for task sessions even if depth metadata is lost. */
|
|
97
|
+
currentAgentType?: string;
|
|
55
98
|
}): boolean {
|
|
99
|
+
if ((input.taskDepth ?? 0) > 0 || input.parentTaskPrefix || input.currentAgentType) return false;
|
|
100
|
+
if (completionNotifyDisabledByEnv(input.env)) return false;
|
|
56
101
|
if (input.env.SKC_NOTIFICATIONS === "0") return false;
|
|
57
102
|
if (input.env.SKC_NOTIFICATIONS === "1" || input.env.SKC_NOTIFICATIONS_TOKEN) return true;
|
|
58
103
|
return input.cfg ? isGloballyConfigured(input.cfg) : false;
|
|
@@ -81,6 +126,7 @@ export function isSessionNotificationsEnabled(input: {
|
|
|
81
126
|
/** Mask a bot token for display: first 4 chars + "…" + "(len N)"; "(unset)" when undefined/empty. Never reveal full token. */
|
|
82
127
|
export function maskToken(token: string | undefined): string {
|
|
83
128
|
if (!token) return "(unset)";
|
|
129
|
+
if (token.length <= 4) return `…(len ${token.length})`;
|
|
84
130
|
return `${token.slice(0, 4)}…(len ${token.length})`;
|
|
85
131
|
}
|
|
86
132
|
|
|
@@ -112,7 +158,8 @@ export interface RedactableAction {
|
|
|
112
158
|
* When redact is false, return the action unchanged.
|
|
113
159
|
*
|
|
114
160
|
* Redaction still applies to streamed content frames (turn_stream, context_update,
|
|
115
|
-
* image_attachment) which are suppressed at their emit sites, not here.
|
|
161
|
+
* image_attachment) which are suppressed at their emit sites, not here. Explicit
|
|
162
|
+
* `telegram_send` file attachments are rejected before the file is read or forwarded.
|
|
116
163
|
*/
|
|
117
164
|
export function buildRedactedAction(
|
|
118
165
|
action: RedactableAction,
|
|
@@ -123,6 +170,6 @@ export function buildRedactedAction(
|
|
|
123
170
|
// Asks stay fully readable/answerable even under redaction.
|
|
124
171
|
if (action.kind === "ask") return action;
|
|
125
172
|
|
|
126
|
-
const { summary: _summary, question: _question, ...base } = action;
|
|
173
|
+
const { summary: _summary, question: _question, options: _options, ...base } = action;
|
|
127
174
|
return base;
|
|
128
175
|
}
|
|
@@ -7,6 +7,7 @@ export interface DaemonPaths {
|
|
|
7
7
|
roots: string;
|
|
8
8
|
steal: string;
|
|
9
9
|
aliases: string;
|
|
10
|
+
seenUpdates: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function daemonPaths(agentDir: string): DaemonPaths {
|
|
@@ -18,5 +19,6 @@ export function daemonPaths(agentDir: string): DaemonPaths {
|
|
|
18
19
|
roots: path.join(dir, "telegram-daemon.roots.json"),
|
|
19
20
|
steal: path.join(dir, "telegram-daemon.steal"),
|
|
20
21
|
aliases: path.join(dir, "telegram-callback-aliases.json"),
|
|
22
|
+
seenUpdates: path.join(dir, "telegram-seen-updates.json"),
|
|
21
23
|
};
|
|
22
24
|
}
|
|
@@ -28,13 +28,13 @@ import type { ImageContent, TextContent } from "@sayknow-cli/ai";
|
|
|
28
28
|
import { NotificationServer } from "@sayknow-cli/natives";
|
|
29
29
|
import { logger, postmortem } from "@sayknow-cli/utils";
|
|
30
30
|
import { Settings } from "../config/settings";
|
|
31
|
-
import type { ExtensionCommandContext, ExtensionContext
|
|
31
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../extensibility/extensions";
|
|
32
32
|
import { registerAskAnswerSource } from "../tools/ask-answer-registry";
|
|
33
33
|
import { registerTelegramFileSink } from "./attachment-registry";
|
|
34
34
|
import {
|
|
35
35
|
getNotificationConfig,
|
|
36
|
-
isGloballyConfigured,
|
|
37
36
|
isSessionNotificationsEnabled,
|
|
37
|
+
isTelegramConfigured,
|
|
38
38
|
type NotificationConfig,
|
|
39
39
|
sessionTag,
|
|
40
40
|
} from "./config";
|
|
@@ -90,6 +90,8 @@ export interface SessionCreateFrame {
|
|
|
90
90
|
target: SessionCreateTarget;
|
|
91
91
|
/** Reference to the daemon-written, once-consumed startup-prompt file. */
|
|
92
92
|
startupPromptRef?: string;
|
|
93
|
+
/** Model profile preset to activate for the spawned session (--mpreset). */
|
|
94
|
+
modelPreset?: string;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
/** Close (hard-kill, history preserved) a session. */
|
|
@@ -100,7 +102,7 @@ export interface SessionCloseFrame {
|
|
|
100
102
|
chatId: string;
|
|
101
103
|
token: string;
|
|
102
104
|
target: SessionCloseTarget;
|
|
103
|
-
/**
|
|
105
|
+
/** Required force-only close flag; false/omitted is rejected by daemon policy. */
|
|
104
106
|
force?: boolean;
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -336,6 +338,8 @@ interface SessionRuntime {
|
|
|
336
338
|
disposeAnswerSource: () => void;
|
|
337
339
|
/** Deregisters this session's Telegram file sink. */
|
|
338
340
|
disposeFileSink: () => void;
|
|
341
|
+
/** Deregisters this session's unattended workflow-gate listener. */
|
|
342
|
+
disposeGateListener: () => void;
|
|
339
343
|
redact: boolean;
|
|
340
344
|
verbosity: "lean" | "verbose";
|
|
341
345
|
sessionTag: string;
|
|
@@ -348,6 +352,15 @@ interface SessionRuntime {
|
|
|
348
352
|
/** Assistant text already flushed before an ask this turn (turn-scoped dedupe
|
|
349
353
|
* so turn_end does not re-emit the pre-ask lead-in). Reset each turn. */
|
|
350
354
|
preAskFlushedText?: string;
|
|
355
|
+
/** Live streaming: opt-in flag, monotonic per-turn ref, and emit throttle state. */
|
|
356
|
+
stream: boolean;
|
|
357
|
+
turnSeq?: number;
|
|
358
|
+
liveRef?: string;
|
|
359
|
+
lastLiveAt?: number;
|
|
360
|
+
lastLiveText?: string;
|
|
361
|
+
/** True between turn_end and the next turn_start: drops late async message_update
|
|
362
|
+
* frames so a stale live edit can never be emitted after the finalized turn. */
|
|
363
|
+
turnClosed?: boolean;
|
|
351
364
|
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
352
365
|
cancelPostmortemCleanup: () => void;
|
|
353
366
|
}
|
|
@@ -358,6 +371,8 @@ interface ResolvedSettings {
|
|
|
358
371
|
settingsAvailable: boolean;
|
|
359
372
|
}
|
|
360
373
|
|
|
374
|
+
const TELEGRAM_FILE_REDACTION_ERROR = "Telegram file attachments are disabled while notifications redaction is on.";
|
|
375
|
+
|
|
361
376
|
const defaultConfig: NotificationConfig = {
|
|
362
377
|
enabled: false,
|
|
363
378
|
botToken: undefined,
|
|
@@ -373,13 +388,44 @@ const defaultConfig: NotificationConfig = {
|
|
|
373
388
|
redact: false,
|
|
374
389
|
verbosity: "lean",
|
|
375
390
|
idleTimeoutMs: 60_000,
|
|
391
|
+
rich: { enabled: true },
|
|
392
|
+
richDraft: { enabled: false },
|
|
376
393
|
};
|
|
377
394
|
|
|
378
395
|
export function notificationsEnabled(): boolean {
|
|
379
396
|
return process.env.SKC_NOTIFICATIONS === "1" || Boolean(process.env.SKC_NOTIFICATIONS_TOKEN);
|
|
380
397
|
}
|
|
381
398
|
|
|
382
|
-
|
|
399
|
+
// Live streaming (opt-in): emit throttled non-finalized `turn_stream` frames as
|
|
400
|
+
// the assistant message streams so remote clients can edit ONE message live. The
|
|
401
|
+
// finalized frame (turn_end) carries the same messageRef and stays authoritative,
|
|
402
|
+
// so a dropped live frame self-heals. Off unless SKC_NOTIFICATIONS_STREAM=1.
|
|
403
|
+
function streamingEnabled(): boolean {
|
|
404
|
+
return process.env.SKC_NOTIFICATIONS_STREAM === "1";
|
|
405
|
+
}
|
|
406
|
+
function streamIntervalMs(): number {
|
|
407
|
+
return Math.max(200, Number(process.env.SKC_NOTIFICATIONS_STREAM_INTERVAL_MS) || 500);
|
|
408
|
+
}
|
|
409
|
+
// Max chars of a turn's assistant text carried by the FINALIZED turn_stream (and
|
|
410
|
+
// the pre-ask capture). Default 3500 keeps the mirror a glanceable per-turn
|
|
411
|
+
// summary; a client that splits long messages (the Telegram daemon does so via
|
|
412
|
+
// splitTelegramHtml, scheduling each chunk through the shared rate-limit pool so
|
|
413
|
+
// the fan-out never bypasses the per-chat limit) can raise it with
|
|
414
|
+
// SKC_NOTIFICATIONS_TURN_MAX to deliver full turns. The value is clamped to a
|
|
415
|
+
// finite [280, TURN_TEXT_MAX_CEILING] range: a non-finite or non-positive env
|
|
416
|
+
// (unset, NaN, Infinity, <= 0) falls back to the default, so the cap can never
|
|
417
|
+
// be unbounded. Live frames are intentionally NOT raised — they stay one
|
|
418
|
+
// editable preview message rather than fanning a long in-progress turn across
|
|
419
|
+
// sends.
|
|
420
|
+
const TURN_TEXT_MAX_CEILING = 40_000;
|
|
421
|
+
function turnTextMax(): number {
|
|
422
|
+
const raw = Number(process.env.SKC_NOTIFICATIONS_TURN_MAX);
|
|
423
|
+
if (!Number.isFinite(raw) || raw <= 0) return 3500;
|
|
424
|
+
return Math.min(TURN_TEXT_MAX_CEILING, Math.max(280, raw));
|
|
425
|
+
}
|
|
426
|
+
function resolveSettings(settingsOverride?: Settings): ResolvedSettings {
|
|
427
|
+
if (settingsOverride)
|
|
428
|
+
return { settings: settingsOverride, cfg: getNotificationConfig(settingsOverride), settingsAvailable: true };
|
|
383
429
|
try {
|
|
384
430
|
const settings = Settings.instance;
|
|
385
431
|
return { settings, cfg: getNotificationConfig(settings), settingsAvailable: true };
|
|
@@ -491,7 +537,7 @@ function sessionIdFromFile(file: string | undefined): string | undefined {
|
|
|
491
537
|
return underscore >= 0 ? base.slice(underscore + 1) : undefined;
|
|
492
538
|
}
|
|
493
539
|
|
|
494
|
-
export
|
|
540
|
+
export function createNotificationsExtension(api: ExtensionAPI, options: { settings?: Settings } = {}): void {
|
|
495
541
|
const runtimes = new Map<string, SessionRuntime>();
|
|
496
542
|
const disabledSessions = new Set<string>();
|
|
497
543
|
const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
|
|
@@ -503,9 +549,16 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
503
549
|
runtimes.delete(id);
|
|
504
550
|
try {
|
|
505
551
|
rt.cancelPostmortemCleanup();
|
|
552
|
+
} catch {}
|
|
553
|
+
try {
|
|
506
554
|
rt.disposeAnswerSource();
|
|
555
|
+
} catch {}
|
|
556
|
+
try {
|
|
507
557
|
rt.disposeFileSink();
|
|
508
558
|
} catch {}
|
|
559
|
+
try {
|
|
560
|
+
rt.disposeGateListener();
|
|
561
|
+
} catch {}
|
|
509
562
|
// Resolve any still-pending interactive asks so the ask tool is not left hanging.
|
|
510
563
|
for (const pending of rt.pendingInteractive.values()) pending.resolve(undefined);
|
|
511
564
|
rt.pendingInteractive.clear();
|
|
@@ -529,10 +582,14 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
529
582
|
return isSessionNotificationsEnabled({ cfg, env: process.env, sessionDisabled: disabledSessions.has(id) });
|
|
530
583
|
}
|
|
531
584
|
|
|
585
|
+
function isNotificationEligibleContext(ctx: ExtensionContext): boolean {
|
|
586
|
+
return ctx.sessionMetadata?.kind !== "sub";
|
|
587
|
+
}
|
|
588
|
+
|
|
532
589
|
async function startSession(ctx: ExtensionContext): Promise<"started" | "already" | "disabled" | "failed"> {
|
|
533
590
|
const id = sessionId(ctx);
|
|
534
|
-
const { settings, cfg, settingsAvailable } = resolveSettings();
|
|
535
|
-
if (!isEnabledForSession(id, cfg)) return "disabled";
|
|
591
|
+
const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
|
|
592
|
+
if (!isNotificationEligibleContext(ctx) || !isEnabledForSession(id, cfg)) return "disabled";
|
|
536
593
|
if (runtimes.has(id)) return "already";
|
|
537
594
|
|
|
538
595
|
const stateRoot = path.join(ctx.cwd, ".skc", "state");
|
|
@@ -660,6 +717,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
660
717
|
tag,
|
|
661
718
|
);
|
|
662
719
|
const disposeFileSink = registerTelegramFileSink(id, async file => {
|
|
720
|
+
if (runtime?.redact ?? redact) {
|
|
721
|
+
return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
722
|
+
}
|
|
723
|
+
|
|
663
724
|
try {
|
|
664
725
|
const data = await fs.promises.readFile(file.path);
|
|
665
726
|
server.pushFrame(
|
|
@@ -683,9 +744,11 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
683
744
|
pendingInteractive,
|
|
684
745
|
disposeAnswerSource,
|
|
685
746
|
disposeFileSink,
|
|
747
|
+
disposeGateListener: () => {},
|
|
686
748
|
cancelPostmortemCleanup: () => {},
|
|
687
749
|
redact,
|
|
688
750
|
verbosity,
|
|
751
|
+
stream: streamingEnabled(),
|
|
689
752
|
sessionTag: tag,
|
|
690
753
|
busy: false,
|
|
691
754
|
pendingInbound: new Set<number>(),
|
|
@@ -702,7 +765,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
702
765
|
});
|
|
703
766
|
logger.info(`notifications: serving session ${id} at ${endpoint.url} (unattended=${unattended})`);
|
|
704
767
|
|
|
705
|
-
if (settingsAvailable && settings &&
|
|
768
|
+
if (settingsAvailable && settings && isTelegramConfigured(cfg)) {
|
|
706
769
|
try {
|
|
707
770
|
await ensureTelegramDaemonRunning({ settings, cwd: ctx.cwd, sessionId: id });
|
|
708
771
|
} catch (e) {
|
|
@@ -726,7 +789,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
726
789
|
|
|
727
790
|
// Unattended: a real ask emits a workflow gate; register it repliable by gate_id.
|
|
728
791
|
if (unattended && gate?.onGateEmitted) {
|
|
729
|
-
gate.onGateEmitted(g => {
|
|
792
|
+
runtime.disposeGateListener = gate.onGateEmitted(g => {
|
|
730
793
|
const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
|
|
731
794
|
gateOptions.set(g.gate_id, options);
|
|
732
795
|
const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
|
|
@@ -761,7 +824,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
761
824
|
async handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
762
825
|
const id = sessionId(ctx);
|
|
763
826
|
const command = args.trim().split(/\s+/, 1)[0]?.toLowerCase() || "status";
|
|
764
|
-
const resolved = resolveSettings();
|
|
827
|
+
const resolved = resolveSettings(options.settings);
|
|
765
828
|
const enabledWithoutLocalOff = isSessionNotificationsEnabled({
|
|
766
829
|
cfg: resolved.cfg,
|
|
767
830
|
env: process.env,
|
|
@@ -781,6 +844,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
781
844
|
}
|
|
782
845
|
|
|
783
846
|
if (command === "on") {
|
|
847
|
+
if (!isNotificationEligibleContext(ctx)) {
|
|
848
|
+
ctx.ui.notify("Notifications are disabled for subagent sessions.", "warning");
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
784
851
|
if (process.env.SKC_NOTIFICATIONS === "0") {
|
|
785
852
|
ctx.ui.notify(
|
|
786
853
|
"Notifications remain disabled: SKC_NOTIFICATIONS=0 is an authoritative opt-out.",
|
|
@@ -878,6 +945,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
878
945
|
rt.sessionTag,
|
|
879
946
|
);
|
|
880
947
|
rt.disposeFileSink = registerTelegramFileSink(newId, async file => {
|
|
948
|
+
if (rt.redact) {
|
|
949
|
+
return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
950
|
+
}
|
|
951
|
+
|
|
881
952
|
try {
|
|
882
953
|
const data = await fs.promises.readFile(file.path);
|
|
883
954
|
rt.server.pushFrame(
|
|
@@ -933,7 +1004,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
933
1004
|
api.on("turn_start", (_event, ctx) => {
|
|
934
1005
|
const id = sessionId(ctx);
|
|
935
1006
|
const rt = runtimes.get(id);
|
|
936
|
-
if (!rt
|
|
1007
|
+
if (!rt) return;
|
|
1008
|
+
// A new turn is live: re-open the live-stream window (see turnClosed).
|
|
1009
|
+
rt.turnClosed = false;
|
|
1010
|
+
if (rt.pendingInbound.size === 0) return;
|
|
937
1011
|
for (const updateId of rt.pendingInbound) {
|
|
938
1012
|
try {
|
|
939
1013
|
rt.server.pushFrame(JSON.stringify({ type: "inbound_ack", sessionId: id, updateId, state: "consumed" }));
|
|
@@ -1029,11 +1103,29 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
1029
1103
|
// rate-limit pool before sending to Telegram.
|
|
1030
1104
|
// Push the in-flight turn's assistant text as a finalized turn_stream, deduped
|
|
1031
1105
|
// against what was already flushed for this turn (the pre-ask lead-in).
|
|
1032
|
-
const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined): void => {
|
|
1106
|
+
const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined, finalAnswer: boolean): void => {
|
|
1033
1107
|
if (!text || text === rt.preAskFlushedText) return;
|
|
1034
1108
|
rt.preAskFlushedText = text;
|
|
1109
|
+
// Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
|
|
1110
|
+
// live message, never a fresh (rich-promotable) send. If live frames were
|
|
1111
|
+
// async-queued and none landed before this flush, allocate the per-turn ref
|
|
1112
|
+
// now so the finalized frame always carries a messageRef → the daemon keeps it
|
|
1113
|
+
// editable (HTML edit) and never rich-promotes a streamed final.
|
|
1114
|
+
if (finalAnswer && rt.stream && rt.liveRef === undefined) {
|
|
1115
|
+
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
1116
|
+
rt.liveRef = String(rt.turnSeq);
|
|
1117
|
+
}
|
|
1035
1118
|
try {
|
|
1036
|
-
rt.server.pushFrame(
|
|
1119
|
+
rt.server.pushFrame(
|
|
1120
|
+
JSON.stringify({
|
|
1121
|
+
type: "turn_stream",
|
|
1122
|
+
sessionId: id,
|
|
1123
|
+
phase: "finalized",
|
|
1124
|
+
finalAnswer,
|
|
1125
|
+
text,
|
|
1126
|
+
...(rt.liveRef ? { messageRef: rt.liveRef } : {}),
|
|
1127
|
+
}),
|
|
1128
|
+
);
|
|
1037
1129
|
} catch (e) {
|
|
1038
1130
|
logger.warn(`notifications: pushFrame (turn) failed: ${String(e)}`);
|
|
1039
1131
|
}
|
|
@@ -1050,19 +1142,53 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
1050
1142
|
const id = sessionId(ctx);
|
|
1051
1143
|
const rt = runtimes.get(id);
|
|
1052
1144
|
if (!rt || rt.redact) return;
|
|
1053
|
-
flushTurnText(rt, id, rt.currentTurnText);
|
|
1145
|
+
flushTurnText(rt, id, rt.currentTurnText, false);
|
|
1054
1146
|
});
|
|
1055
1147
|
|
|
1056
1148
|
api.on("turn_end", (event, ctx) => {
|
|
1057
1149
|
const id = sessionId(ctx);
|
|
1058
1150
|
const rt = runtimes.get(id);
|
|
1059
1151
|
if (!rt) return;
|
|
1060
|
-
const text = rt.redact ? undefined : summaryFromMessage(event.message,
|
|
1061
|
-
if (text) flushTurnText(rt, id, text);
|
|
1152
|
+
const text = rt.redact ? undefined : summaryFromMessage(event.message, turnTextMax());
|
|
1153
|
+
if (text) flushTurnText(rt, id, text, true);
|
|
1062
1154
|
// Reset per-turn streaming state so the next turn starts fresh and a later
|
|
1063
1155
|
// turn with identical text is not falsely deduped.
|
|
1064
1156
|
rt.currentTurnText = undefined;
|
|
1065
1157
|
rt.preAskFlushedText = undefined;
|
|
1158
|
+
rt.liveRef = undefined;
|
|
1159
|
+
// Close the live-stream window: any message_update queued after turn_end is
|
|
1160
|
+
// dropped so it can never emit a stale live edit past the finalized turn.
|
|
1161
|
+
rt.turnClosed = true;
|
|
1162
|
+
rt.lastLiveAt = undefined;
|
|
1163
|
+
rt.lastLiveText = undefined;
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
// Live streaming (opt-in): push throttled in-progress assistant text as
|
|
1167
|
+
// non-finalized turn_stream frames so remote clients edit one message as the
|
|
1168
|
+
// turn streams. The finalized frame (turn_end) carries the same messageRef and
|
|
1169
|
+
// lands the authoritative text. Suppressed under redaction.
|
|
1170
|
+
api.on("message_update", (event, ctx) => {
|
|
1171
|
+
const id = sessionId(ctx);
|
|
1172
|
+
const rt = runtimes.get(id);
|
|
1173
|
+
if (!rt?.stream || rt.redact || rt.turnClosed) return;
|
|
1174
|
+
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
1175
|
+
if (rt.liveRef === undefined) {
|
|
1176
|
+
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
1177
|
+
rt.liveRef = String(rt.turnSeq);
|
|
1178
|
+
}
|
|
1179
|
+
const now = Date.now();
|
|
1180
|
+
if (now - (rt.lastLiveAt ?? 0) < streamIntervalMs()) return;
|
|
1181
|
+
const text = summaryFromMessage(event.message, 3500);
|
|
1182
|
+
if (!text || text === rt.lastLiveText) return;
|
|
1183
|
+
rt.lastLiveAt = now;
|
|
1184
|
+
rt.lastLiveText = text;
|
|
1185
|
+
try {
|
|
1186
|
+
rt.server.pushFrame(
|
|
1187
|
+
JSON.stringify({ type: "turn_stream", sessionId: id, phase: "live", text, messageRef: rt.liveRef }),
|
|
1188
|
+
);
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
logger.warn(`notifications: pushFrame (live) failed: ${String(e)}`);
|
|
1191
|
+
}
|
|
1066
1192
|
});
|
|
1067
1193
|
|
|
1068
1194
|
// Stream agent-produced images (computer/browser/tool screenshots) as
|
|
@@ -1076,7 +1202,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
1076
1202
|
// flush can emit it before the ask prompt. Role-scoped: message_end also
|
|
1077
1203
|
// fires for the user prompt, which must never be mirrored back as turn output.
|
|
1078
1204
|
if ((event.message as { role?: unknown }).role === "assistant") {
|
|
1079
|
-
const turnText = summaryFromMessage(event.message,
|
|
1205
|
+
const turnText = summaryFromMessage(event.message, turnTextMax());
|
|
1080
1206
|
if (turnText) rt.currentTurnText = turnText;
|
|
1081
1207
|
}
|
|
1082
1208
|
for (const img of imageAttachmentsFromMessage(event.message)) {
|
|
@@ -1099,4 +1225,4 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
1099
1225
|
api.on("session_shutdown", async (_event, ctx) => {
|
|
1100
1226
|
await stopSession(sessionId(ctx));
|
|
1101
1227
|
});
|
|
1102
|
-
}
|
|
1228
|
+
}
|