@sayknow-cli/coding-agent 0.3.0 → 0.3.2
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/bin/skc.js +4 -0
- package/dist/types/cli/mcp-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +2 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/mcp.d.ts +70 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/defaults/skc-defaults.d.ts +2 -13
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +8 -0
- package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- package/dist/types/notifications/attachment-registry.d.ts +17 -0
- package/dist/types/notifications/chat-adapters.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +9 -1
- package/dist/types/notifications/engine.d.ts +59 -0
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/managed-daemon.d.ts +48 -0
- package/dist/types/notifications/operator-runtime.d.ts +52 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +133 -16
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/threaded-inbound.d.ts +19 -0
- package/dist/types/notifications/threaded-render.d.ts +6 -1
- package/dist/types/notifications/topic-registry.d.ts +12 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +16 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
- package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/composer-bash-policy.d.ts +14 -0
- package/dist/types/tools/fetch.d.ts +23 -0
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/telegram-send.d.ts +32 -0
- package/dist/types/utils/changelog.d.ts +1 -0
- package/dist/types/web/insane/bridge.d.ts +103 -0
- package/dist/types/web/insane/url-guard.d.ts +25 -0
- package/dist/types/web/scrapers/types.d.ts +5 -0
- package/dist/types/web/scrapers/utils.d.ts +7 -1
- package/dist/types/web/search/provider.d.ts +18 -1
- package/dist/types/web/search/providers/insane.d.ts +53 -0
- package/dist/types/web/search/providers/text-citations.d.ts +23 -0
- package/dist/types/web/search/types.d.ts +12 -4
- package/package.json +14 -10
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +180 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/scripts/verify-insane-vendor.ts +132 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/fast-help.ts +30 -2
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli/notify-cli.ts +152 -5
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli.ts +30 -12
- package/src/commands/mcp.ts +117 -0
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/commands/team.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +55 -7
- package/src/config/settings-schema.ts +30 -1
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
- package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
- package/src/defaults/skc/skills/team/SKILL.md +5 -3
- package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
- package/src/defaults/skc-defaults.ts +2 -27
- package/src/export/html/index.ts +2 -2
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/skc-plugins/compiler.ts +351 -0
- package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/skc-plugins/index.ts +9 -0
- package/src/extensibility/skc-plugins/injection.ts +109 -0
- package/src/extensibility/skc-plugins/installer.ts +434 -0
- package/src/extensibility/skc-plugins/loader.ts +3 -1
- package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/skc-plugins/observability.ts +84 -0
- package/src/extensibility/skc-plugins/paths.ts +1 -1
- package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/skc-plugins/registry.ts +180 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/skc-plugins/schema.ts +250 -20
- package/src/extensibility/skc-plugins/session-validation.ts +147 -0
- package/src/extensibility/skc-plugins/types.ts +199 -3
- package/src/extensibility/skc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/hooks/skill-state.ts +57 -0
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/lsp/config.ts +16 -3
- package/src/lsp/defaults.json +7 -0
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -3
- package/src/modes/bridge/bridge-mode.ts +11 -0
- package/src/modes/components/assistant-message.ts +49 -1
- package/src/modes/components/custom-editor.ts +2 -0
- package/src/modes/components/footer.ts +2 -3
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +56 -11
- package/src/modes/components/status-line/git-utils.ts +25 -0
- package/src/modes/components/status-line.ts +10 -11
- package/src/modes/components/welcome.ts +2 -3
- package/src/modes/controllers/event-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +53 -11
- package/src/modes/interactive-mode.ts +50 -3
- package/src/modes/shared/agent-wire/scopes.ts +1 -1
- package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/attachment-registry.ts +23 -0
- package/src/notifications/chat-adapters.ts +147 -0
- package/src/notifications/config.ts +23 -2
- package/src/notifications/engine.ts +100 -0
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +417 -45
- package/src/notifications/lifecycle-commands.ts +238 -0
- package/src/notifications/lifecycle-control-runtime.ts +405 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/managed-daemon.ts +163 -0
- package/src/notifications/operator-runtime.ts +171 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +984 -242
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/threaded-inbound.ts +60 -4
- package/src/notifications/threaded-render.ts +20 -2
- package/src/notifications/topic-registry.ts +23 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +157 -10
- package/src/session/agent-session.ts +248 -125
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +762 -12
- package/src/skc-runtime/launch-tmux.ts +85 -22
- package/src/skc-runtime/ledger-event-renderer.ts +1 -0
- package/src/skc-runtime/psmux-detect.ts +239 -0
- package/src/skc-runtime/ralplan-runtime.ts +2 -2
- package/src/skc-runtime/team-runtime.ts +56 -23
- package/src/skc-runtime/tmux-common.ts +88 -4
- package/src/skc-runtime/tmux-sessions.ts +111 -9
- package/src/skc-runtime/ultragoal-guard.ts +25 -8
- package/src/skc-runtime/ultragoal-runtime.ts +75 -15
- package/src/skc-runtime/workflow-manifest.generated.json +29 -0
- package/src/skc-runtime/workflow-manifest.ts +7 -2
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/slash-commands/helpers/parse.ts +2 -1
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/bash.ts +9 -0
- package/src/tools/composer-bash-policy.ts +96 -0
- package/src/tools/fetch.ts +94 -1
- package/src/tools/index.ts +6 -1
- package/src/tools/telegram-send.ts +137 -0
- package/src/utils/changelog.ts +8 -0
- package/src/web/insane/bridge.ts +350 -0
- package/src/web/insane/url-guard.ts +159 -0
- package/src/web/scrapers/types.ts +143 -45
- package/src/web/scrapers/utils.ts +70 -19
- package/src/web/search/provider.ts +77 -18
- package/src/web/search/providers/anthropic.ts +70 -3
- package/src/web/search/providers/codex.ts +1 -119
- package/src/web/search/providers/gemini.ts +99 -0
- package/src/web/search/providers/insane.ts +551 -0
- package/src/web/search/providers/openai-compatible.ts +66 -32
- package/src/web/search/providers/text-citations.ts +111 -0
- package/src/web/search/types.ts +13 -2
- package/vendor/insane-search/LICENSE +21 -0
- package/vendor/insane-search/MANIFEST.json +24 -0
- package/vendor/insane-search/engine/__init__.py +23 -0
- package/vendor/insane-search/engine/__main__.py +128 -0
- package/vendor/insane-search/engine/bias_check.py +183 -0
- package/vendor/insane-search/engine/executor.py +254 -0
- package/vendor/insane-search/engine/fetch_chain.py +725 -0
- package/vendor/insane-search/engine/learning.py +175 -0
- package/vendor/insane-search/engine/phase0.py +214 -0
- package/vendor/insane-search/engine/safety.py +91 -0
- package/vendor/insane-search/engine/templates/package.json +11 -0
- package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
- package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
- package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
- package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
- package/vendor/insane-search/engine/tests/test_u1.py +200 -0
- package/vendor/insane-search/engine/tests/test_u4.py +131 -0
- package/vendor/insane-search/engine/tests/test_u5.py +163 -0
- package/vendor/insane-search/engine/tests/test_u7.py +124 -0
- package/vendor/insane-search/engine/transport.py +211 -0
- package/vendor/insane-search/engine/url_transforms.py +98 -0
- package/vendor/insane-search/engine/validators.py +331 -0
- package/vendor/insane-search/engine/waf_detector.py +214 -0
- package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - `ask` (unattended/RPC): observes emitted workflow gates and resolves the real
|
|
13
13
|
* gate on a remote reply via `ctx.workflowGate`.
|
|
14
14
|
* - `turn_end` -> `action_needed` (kind `idle`, deduped per turn).
|
|
15
|
-
* - `session_shutdown` -> stop
|
|
15
|
+
* - `session_shutdown` -> `session_closed` frame, stop server, deregister answer source.
|
|
16
16
|
*
|
|
17
17
|
* Enable with Settings notifications config, `SKC_NOTIFICATIONS=1` (a token is
|
|
18
18
|
* generated), or `SKC_NOTIFICATIONS_TOKEN`.
|
|
@@ -24,11 +24,13 @@ import * as fs from "node:fs";
|
|
|
24
24
|
import * as os from "node:os";
|
|
25
25
|
import * as path from "node:path";
|
|
26
26
|
import { promisify } from "node:util";
|
|
27
|
+
import type { ImageContent, TextContent } from "@sayknow-cli/ai";
|
|
27
28
|
import { NotificationServer } from "@sayknow-cli/natives";
|
|
28
29
|
import { logger } from "@sayknow-cli/utils";
|
|
29
30
|
import { Settings } from "../config/settings";
|
|
30
31
|
import type { ExtensionCommandContext, ExtensionContext, ExtensionFactory } from "../extensibility/extensions";
|
|
31
32
|
import { registerAskAnswerSource } from "../tools/ask-answer-registry";
|
|
33
|
+
import { registerTelegramFileSink } from "./attachment-registry";
|
|
32
34
|
import {
|
|
33
35
|
getNotificationConfig,
|
|
34
36
|
isGloballyConfigured,
|
|
@@ -39,6 +41,193 @@ import {
|
|
|
39
41
|
import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage } from "./helpers";
|
|
40
42
|
import { ensureTelegramDaemonRunning } from "./telegram-daemon";
|
|
41
43
|
|
|
44
|
+
// ===========================================================================
|
|
45
|
+
// Session lifecycle control protocol (TypeScript mirror of the Rust wire
|
|
46
|
+
// contract in `crates/skc-notifications/src/lifecycle.rs`).
|
|
47
|
+
//
|
|
48
|
+
// These describe the frames exchanged over the daemon-owned, session-independent
|
|
49
|
+
// control endpoint for remote session create / close / resume. Field names are
|
|
50
|
+
// camelCase on the wire; `type`/`kind` discriminators are snake_case. The Rust
|
|
51
|
+
// ingress authenticates and forwards; the daemon (TypeScript) owns all policy,
|
|
52
|
+
// spawn orchestration, idempotency, rate limiting, audit, and UX.
|
|
53
|
+
// ===========================================================================
|
|
54
|
+
|
|
55
|
+
/** Where a `session_create` should run. Discriminated by `kind`. */
|
|
56
|
+
export type SessionCreateTarget =
|
|
57
|
+
| { kind: "existing_path"; path: string }
|
|
58
|
+
| { kind: "worktree"; repo: string; branch: string }
|
|
59
|
+
| { kind: "plain_dir"; path: string };
|
|
60
|
+
|
|
61
|
+
/** Identifies the session a `session_close` targets. */
|
|
62
|
+
export interface SessionCloseTarget {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
/** Expected SKC-managed tmux session name (defense-in-depth match). */
|
|
65
|
+
tmuxSession?: string;
|
|
66
|
+
/** Expected `@skc-session-state-file` tag (defense-in-depth match). */
|
|
67
|
+
sessionStateFile?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Identifies the session a `session_resume` targets. */
|
|
71
|
+
export interface SessionResumeTarget {
|
|
72
|
+
sessionIdOrPrefix: string;
|
|
73
|
+
/** Optional repo/working-dir hint to disambiguate matches. */
|
|
74
|
+
path?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Create a new session. */
|
|
78
|
+
export interface SessionCreateFrame {
|
|
79
|
+
type: "session_create";
|
|
80
|
+
requestId: string;
|
|
81
|
+
/** Deterministic lifecycle marker preallocated by the daemon before spawn. */
|
|
82
|
+
lifecycleRequestId: string;
|
|
83
|
+
/** Session id the daemon preallocated and propagates to the child. */
|
|
84
|
+
intendedSessionId: string;
|
|
85
|
+
/** Telegram update id (idempotency key on the daemon side). */
|
|
86
|
+
updateId: number;
|
|
87
|
+
chatId: string;
|
|
88
|
+
/** Control-endpoint token authorizing this frame. */
|
|
89
|
+
token: string;
|
|
90
|
+
target: SessionCreateTarget;
|
|
91
|
+
/** Reference to the daemon-written, once-consumed startup-prompt file. */
|
|
92
|
+
startupPromptRef?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Close (hard-kill, history preserved) a session. */
|
|
96
|
+
export interface SessionCloseFrame {
|
|
97
|
+
type: "session_close";
|
|
98
|
+
requestId: string;
|
|
99
|
+
updateId: number;
|
|
100
|
+
chatId: string;
|
|
101
|
+
token: string;
|
|
102
|
+
target: SessionCloseTarget;
|
|
103
|
+
/** Hard-kill even if a live pane is attached (SKC-managed only). */
|
|
104
|
+
force?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Resume a session (reattach if alive, else cold-restart from history). */
|
|
108
|
+
export interface SessionResumeFrame {
|
|
109
|
+
type: "session_resume";
|
|
110
|
+
requestId: string;
|
|
111
|
+
updateId: number;
|
|
112
|
+
chatId: string;
|
|
113
|
+
token: string;
|
|
114
|
+
target: SessionResumeTarget;
|
|
115
|
+
startupPromptRef?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Any client -> ingress lifecycle request frame. */
|
|
119
|
+
export type SessionLifecycleRequest = SessionCreateFrame | SessionCloseFrame | SessionResumeFrame;
|
|
120
|
+
|
|
121
|
+
/** Terminal status of a lifecycle request. */
|
|
122
|
+
export type LifecycleStatus = "ok" | "error";
|
|
123
|
+
|
|
124
|
+
/** A connected session's per-session endpoint, returned to the control client. */
|
|
125
|
+
export interface LifecycleEndpoint {
|
|
126
|
+
url: string;
|
|
127
|
+
token: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The Telegram topic/thread a session is surfaced in. */
|
|
131
|
+
export interface LifecycleTopic {
|
|
132
|
+
chatId: string;
|
|
133
|
+
threadId: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** How a create request was correlated to its spawned session. */
|
|
137
|
+
export type MatchedBy = "spawn_marker" | "session_ready";
|
|
138
|
+
|
|
139
|
+
/** Response to a successful `session_create`. */
|
|
140
|
+
export interface SessionCreateResponseFrame {
|
|
141
|
+
type: "session_create_response";
|
|
142
|
+
requestId: string;
|
|
143
|
+
status: LifecycleStatus;
|
|
144
|
+
lifecycleRequestId: string;
|
|
145
|
+
sessionId: string;
|
|
146
|
+
matchedBy: MatchedBy;
|
|
147
|
+
endpoint: LifecycleEndpoint;
|
|
148
|
+
topic: LifecycleTopic;
|
|
149
|
+
target: SessionCreateTarget;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Response to a successful `session_close`. */
|
|
153
|
+
export interface SessionCloseResponseFrame {
|
|
154
|
+
type: "session_close_response";
|
|
155
|
+
requestId: string;
|
|
156
|
+
status: LifecycleStatus;
|
|
157
|
+
sessionId: string;
|
|
158
|
+
processGone: boolean;
|
|
159
|
+
historyPreserved: boolean;
|
|
160
|
+
endpointStale: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Whether a resume reattached to a live session or cold-restarted a dead one. */
|
|
164
|
+
export type ResumeMode = "reattached" | "cold_restarted";
|
|
165
|
+
|
|
166
|
+
/** Response to a successful `session_resume`. */
|
|
167
|
+
export interface SessionResumeResponseFrame {
|
|
168
|
+
type: "session_resume_response";
|
|
169
|
+
requestId: string;
|
|
170
|
+
status: LifecycleStatus;
|
|
171
|
+
sessionId: string;
|
|
172
|
+
mode: ResumeMode;
|
|
173
|
+
endpoint: LifecycleEndpoint;
|
|
174
|
+
topic: LifecycleTopic;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Machine-readable reason a lifecycle request failed. */
|
|
178
|
+
export type LifecycleErrorReason =
|
|
179
|
+
| "unauthorized"
|
|
180
|
+
| "rate_limited"
|
|
181
|
+
| "duplicate_conflict"
|
|
182
|
+
| "invalid_target"
|
|
183
|
+
| "ambiguous_target"
|
|
184
|
+
| "spawn_failed"
|
|
185
|
+
| "discovery_timeout"
|
|
186
|
+
| "readiness_timeout"
|
|
187
|
+
| "close_refused"
|
|
188
|
+
| "not_found"
|
|
189
|
+
| "terminal_uncertain";
|
|
190
|
+
|
|
191
|
+
/** A candidate returned with an `ambiguous_target` resume error. */
|
|
192
|
+
export interface ResumeCandidate {
|
|
193
|
+
sessionId: string;
|
|
194
|
+
path?: string;
|
|
195
|
+
/** Last-activity epoch-millis (session history file mtime), if known. */
|
|
196
|
+
mtimeMs?: number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A structured lifecycle error frame. */
|
|
200
|
+
export interface SessionLifecycleErrorFrame {
|
|
201
|
+
type: "session_lifecycle_error";
|
|
202
|
+
requestId: string;
|
|
203
|
+
status: LifecycleStatus;
|
|
204
|
+
reason: LifecycleErrorReason;
|
|
205
|
+
message: string;
|
|
206
|
+
candidates?: ResumeCandidate[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Any ingress -> client lifecycle response frame. */
|
|
210
|
+
export type SessionLifecycleResponse =
|
|
211
|
+
| SessionCreateResponseFrame
|
|
212
|
+
| SessionCloseResponseFrame
|
|
213
|
+
| SessionResumeResponseFrame
|
|
214
|
+
| SessionLifecycleErrorFrame;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Replayable per-session readiness signal (mirror of the Rust `session_ready`
|
|
218
|
+
* frame). Buffered and replayed to late clients so WS-open alone never implies
|
|
219
|
+
* the session is live and surfaced.
|
|
220
|
+
*/
|
|
221
|
+
export interface SessionReadyFrame {
|
|
222
|
+
type: "session_ready";
|
|
223
|
+
sessionId: string;
|
|
224
|
+
lifecycleRequestId?: string;
|
|
225
|
+
startupPromptRef?: string;
|
|
226
|
+
repo?: string;
|
|
227
|
+
branch?: string;
|
|
228
|
+
title?: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
42
231
|
/** Resolve the git dir for `cwd`, handling worktrees where `.git` is a file. */
|
|
43
232
|
function gitDir(cwd: string): string | undefined {
|
|
44
233
|
const dot = path.join(cwd, ".git");
|
|
@@ -136,7 +325,10 @@ interface SessionRuntime {
|
|
|
136
325
|
pendingInteractive: Map<string, PendingInteractiveAsk>;
|
|
137
326
|
/** Deregisters this session's ask answer source. */
|
|
138
327
|
disposeAnswerSource: () => void;
|
|
328
|
+
/** Deregisters this session's Telegram file sink. */
|
|
329
|
+
disposeFileSink: () => void;
|
|
139
330
|
redact: boolean;
|
|
331
|
+
verbosity: "lean" | "verbose";
|
|
140
332
|
sessionTag: string;
|
|
141
333
|
/** Whether the agent loop is currently running (drives the typing indicator). */
|
|
142
334
|
busy: boolean;
|
|
@@ -157,6 +349,16 @@ interface ResolvedSettings {
|
|
|
157
349
|
|
|
158
350
|
const defaultConfig: NotificationConfig = {
|
|
159
351
|
enabled: false,
|
|
352
|
+
botToken: undefined,
|
|
353
|
+
chatId: undefined,
|
|
354
|
+
discord: {
|
|
355
|
+
botToken: undefined,
|
|
356
|
+
channelId: undefined,
|
|
357
|
+
},
|
|
358
|
+
slack: {
|
|
359
|
+
botToken: undefined,
|
|
360
|
+
channelId: undefined,
|
|
361
|
+
},
|
|
160
362
|
redact: false,
|
|
161
363
|
verbosity: "lean",
|
|
162
364
|
idleTimeoutMs: 60_000,
|
|
@@ -228,21 +430,81 @@ function mapAnswerToGate(
|
|
|
228
430
|
return { selected: [] };
|
|
229
431
|
}
|
|
230
432
|
|
|
433
|
+
/** Register the interactive `ask` answer source for a session (the ask tool
|
|
434
|
+
* races the local UI against a remote reply). Returns the deregister disposer. */
|
|
435
|
+
function registerInteractiveAnswerSource(
|
|
436
|
+
id: string,
|
|
437
|
+
server: NotificationServer,
|
|
438
|
+
pendingInteractive: Map<string, PendingInteractiveAsk>,
|
|
439
|
+
getRedact: () => boolean,
|
|
440
|
+
tag: string,
|
|
441
|
+
): () => void {
|
|
442
|
+
return registerAskAnswerSource(id, {
|
|
443
|
+
awaitAnswer(question, options, signal) {
|
|
444
|
+
if (signal?.aborted) return Promise.resolve(undefined);
|
|
445
|
+
const askId = `ask:${crypto.randomUUID()}`;
|
|
446
|
+
try {
|
|
447
|
+
server.registerAsk(
|
|
448
|
+
JSON.stringify(
|
|
449
|
+
notificationActionPayload(
|
|
450
|
+
{ id: askId, kind: "ask", sessionId: id, question, options },
|
|
451
|
+
{ redact: getRedact(), sessionTag: tag },
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
true,
|
|
455
|
+
);
|
|
456
|
+
} catch (e) {
|
|
457
|
+
logger.warn(`notifications: registerAsk failed: ${String(e)}`);
|
|
458
|
+
return Promise.resolve(undefined);
|
|
459
|
+
}
|
|
460
|
+
return new Promise<string | undefined>(resolve => {
|
|
461
|
+
pendingInteractive.set(askId, { resolve, options });
|
|
462
|
+
signal?.addEventListener("abort", () => {
|
|
463
|
+
if (!pendingInteractive.delete(askId)) return;
|
|
464
|
+
// Local UI answered: mark the remote action resolved-locally.
|
|
465
|
+
try {
|
|
466
|
+
server.resolveLocal(askId, undefined);
|
|
467
|
+
} catch {}
|
|
468
|
+
resolve(undefined);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Extract the session id from a `<timestamp>_<uuid>.jsonl` session file path. */
|
|
476
|
+
function sessionIdFromFile(file: string | undefined): string | undefined {
|
|
477
|
+
if (!file) return undefined;
|
|
478
|
+
const base = path.basename(file).replace(/\.jsonl$/, "");
|
|
479
|
+
const underscore = base.indexOf("_");
|
|
480
|
+
return underscore >= 0 ? base.slice(underscore + 1) : undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
231
483
|
export const createNotificationsExtension: ExtensionFactory = api => {
|
|
232
484
|
const runtimes = new Map<string, SessionRuntime>();
|
|
233
485
|
const disabledSessions = new Set<string>();
|
|
234
486
|
const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
|
|
487
|
+
const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
|
235
488
|
|
|
236
|
-
function stopSession(id: string): boolean {
|
|
489
|
+
async function stopSession(id: string): Promise<boolean> {
|
|
237
490
|
const rt = runtimes.get(id);
|
|
238
491
|
if (!rt) return false;
|
|
239
492
|
runtimes.delete(id);
|
|
240
493
|
try {
|
|
241
494
|
rt.disposeAnswerSource();
|
|
495
|
+
rt.disposeFileSink();
|
|
242
496
|
} catch {}
|
|
243
497
|
// Resolve any still-pending interactive asks so the ask tool is not left hanging.
|
|
244
498
|
for (const pending of rt.pendingInteractive.values()) pending.resolve(undefined);
|
|
245
499
|
rt.pendingInteractive.clear();
|
|
500
|
+
let closeFrameSent = false;
|
|
501
|
+
try {
|
|
502
|
+
rt.server.pushFrame(JSON.stringify({ type: "session_closed", sessionId: id }));
|
|
503
|
+
closeFrameSent = true;
|
|
504
|
+
} catch (e) {
|
|
505
|
+
logger.warn(`notifications: session_closed failed: ${String(e)}`);
|
|
506
|
+
}
|
|
507
|
+
if (closeFrameSent) await sleep(100);
|
|
246
508
|
try {
|
|
247
509
|
rt.server.stop();
|
|
248
510
|
} catch (e) {
|
|
@@ -271,6 +533,8 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
271
533
|
const pendingInteractive = new Map<string, PendingInteractiveAsk>();
|
|
272
534
|
const tag = sessionTag(id);
|
|
273
535
|
const redact = cfg.redact;
|
|
536
|
+
const verbosity = cfg.verbosity;
|
|
537
|
+
let runtime: SessionRuntime | undefined;
|
|
274
538
|
|
|
275
539
|
// The SDK can always answer now (interactive via the answer source, or the
|
|
276
540
|
// unattended gate), so the endpoint advertises a resolver.
|
|
@@ -317,23 +581,58 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
317
581
|
// thread (forwarded by the daemon over the WS, fail-closed at the daemon).
|
|
318
582
|
server.onInbound((err, inbound) => {
|
|
319
583
|
if (err || !inbound) return;
|
|
320
|
-
if (inbound.kind === "user_message"
|
|
584
|
+
if (inbound.kind === "user_message") {
|
|
321
585
|
// Inject as a user turn (steers/continues the agent; the resulting
|
|
322
586
|
// turn streams back via the turn_end handler even when not idle).
|
|
323
587
|
// Record the update id so it can be acked as "consumed" on the next
|
|
324
588
|
// turn_start, and steer (vs start a fresh turn) when already busy.
|
|
325
|
-
const
|
|
326
|
-
|
|
589
|
+
const text = inbound.text ?? "";
|
|
590
|
+
const images = inbound.images ?? [];
|
|
591
|
+
if (!text && images.length === 0) return;
|
|
592
|
+
if (runtime && typeof inbound.updateId === "number") runtime.pendingInbound.add(inbound.updateId);
|
|
593
|
+
const content: string | (TextContent | ImageContent)[] =
|
|
594
|
+
images.length > 0
|
|
595
|
+
? [
|
|
596
|
+
...(text ? [{ type: "text", text } as TextContent] : []),
|
|
597
|
+
...images.map(
|
|
598
|
+
img =>
|
|
599
|
+
({ type: "image", data: img.data, mimeType: img.mime ?? "image/jpeg" }) as ImageContent,
|
|
600
|
+
),
|
|
601
|
+
]
|
|
602
|
+
: text;
|
|
327
603
|
try {
|
|
328
|
-
api.sendUserMessage(
|
|
604
|
+
api.sendUserMessage(content, runtime?.busy ? { deliverAs: "steer" } : undefined);
|
|
329
605
|
} catch (e) {
|
|
330
606
|
logger.warn(`notifications: sendUserMessage failed: ${String(e)}`);
|
|
331
607
|
}
|
|
332
608
|
return;
|
|
333
609
|
}
|
|
334
610
|
if (inbound.kind === "config_command") {
|
|
335
|
-
|
|
336
|
-
|
|
611
|
+
if (!runtime) return;
|
|
612
|
+
const update: {
|
|
613
|
+
type: "config_update";
|
|
614
|
+
sessionId: string;
|
|
615
|
+
verbosity?: "lean" | "verbose";
|
|
616
|
+
redact?: boolean;
|
|
617
|
+
} = {
|
|
618
|
+
type: "config_update",
|
|
619
|
+
sessionId: id,
|
|
620
|
+
};
|
|
621
|
+
if (inbound.verbosity === "lean" || inbound.verbosity === "verbose") {
|
|
622
|
+
runtime.verbosity = inbound.verbosity;
|
|
623
|
+
update.verbosity = inbound.verbosity;
|
|
624
|
+
}
|
|
625
|
+
if (typeof inbound.redact === "boolean") {
|
|
626
|
+
runtime.redact = inbound.redact;
|
|
627
|
+
update.redact = inbound.redact;
|
|
628
|
+
}
|
|
629
|
+
if (update.verbosity !== undefined || update.redact !== undefined) {
|
|
630
|
+
try {
|
|
631
|
+
runtime.server.pushFrame(JSON.stringify(update));
|
|
632
|
+
} catch (e) {
|
|
633
|
+
logger.warn(`notifications: config_update failed: ${String(e)}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
337
636
|
}
|
|
338
637
|
});
|
|
339
638
|
|
|
@@ -341,48 +640,44 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
341
640
|
const endpoint = await server.start();
|
|
342
641
|
|
|
343
642
|
// Interactive answer source: the ask tool races the local UI against this.
|
|
344
|
-
const disposeAnswerSource =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
try {
|
|
368
|
-
server.resolveLocal(askId, undefined);
|
|
369
|
-
} catch {}
|
|
370
|
-
resolve(undefined);
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
},
|
|
643
|
+
const disposeAnswerSource = registerInteractiveAnswerSource(
|
|
644
|
+
id,
|
|
645
|
+
server,
|
|
646
|
+
pendingInteractive,
|
|
647
|
+
() => runtime?.redact ?? redact,
|
|
648
|
+
tag,
|
|
649
|
+
);
|
|
650
|
+
const disposeFileSink = registerTelegramFileSink(id, async file => {
|
|
651
|
+
try {
|
|
652
|
+
const data = await fs.promises.readFile(file.path);
|
|
653
|
+
server.pushFrame(
|
|
654
|
+
JSON.stringify({
|
|
655
|
+
type: "file_attachment",
|
|
656
|
+
sessionId: id,
|
|
657
|
+
name: path.basename(file.path),
|
|
658
|
+
data: data.toString("base64"),
|
|
659
|
+
caption: file.caption,
|
|
660
|
+
}),
|
|
661
|
+
);
|
|
662
|
+
return { ok: true };
|
|
663
|
+
} catch (e) {
|
|
664
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
665
|
+
}
|
|
374
666
|
});
|
|
375
667
|
|
|
376
|
-
|
|
668
|
+
runtime = {
|
|
377
669
|
server,
|
|
378
670
|
idleSeq: 0,
|
|
379
671
|
pendingInteractive,
|
|
380
672
|
disposeAnswerSource,
|
|
673
|
+
disposeFileSink,
|
|
381
674
|
redact,
|
|
675
|
+
verbosity,
|
|
382
676
|
sessionTag: tag,
|
|
383
677
|
busy: false,
|
|
384
678
|
pendingInbound: new Set<number>(),
|
|
385
|
-
}
|
|
679
|
+
};
|
|
680
|
+
runtimes.set(id, runtime);
|
|
386
681
|
logger.info(`notifications: serving session ${id} at ${endpoint.url} (unattended=${unattended})`);
|
|
387
682
|
|
|
388
683
|
if (settingsAvailable && settings && isGloballyConfigured(cfg)) {
|
|
@@ -453,7 +748,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
453
748
|
|
|
454
749
|
if (command === "off") {
|
|
455
750
|
disabledSessions.add(id);
|
|
456
|
-
const stopped = stopSession(id);
|
|
751
|
+
const stopped = await stopSession(id);
|
|
457
752
|
ctx.ui.notify(
|
|
458
753
|
stopped
|
|
459
754
|
? "Notifications disabled for this session."
|
|
@@ -501,8 +796,9 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
501
796
|
const running = runtimes.has(id);
|
|
502
797
|
const locallyDisabled = disabledSessions.has(id);
|
|
503
798
|
const enabled = isEnabledForSession(id, resolved.cfg);
|
|
799
|
+
const runtime = runtimes.get(id);
|
|
504
800
|
ctx.ui.notify(
|
|
505
|
-
`Notifications ${running ? "running" : enabled ? "enabled" : "disabled"} for this session; redaction ${resolved.cfg.redact ? "on" : "off"}${locallyDisabled ? "; locally off" : ""}.`,
|
|
801
|
+
`Notifications ${running ? "running" : enabled ? "enabled" : "disabled"} for this session; redaction ${(runtime?.redact ?? resolved.cfg.redact) ? "on" : "off"}; verbosity ${runtime?.verbosity ?? resolved.cfg.verbosity}${locallyDisabled ? "; locally off" : ""}.`,
|
|
506
802
|
"info",
|
|
507
803
|
);
|
|
508
804
|
},
|
|
@@ -512,6 +808,82 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
512
808
|
await startSession(ctx);
|
|
513
809
|
});
|
|
514
810
|
|
|
811
|
+
// A session id change within the same process needs reason-aware handling.
|
|
812
|
+
// `/new` and fork CONTINUE the same terminal thread (e.g. plan "approve and
|
|
813
|
+
// execute" clears into a fresh session), so re-key the existing runtime
|
|
814
|
+
// old→new WITHOUT recreating the NotificationServer: the server, its endpoint
|
|
815
|
+
// discovery file, and the daemon's forum topic are all keyed by the original
|
|
816
|
+
// session id and the daemon routes by socket, so the existing topic is reused
|
|
817
|
+
// and the next identity frame renames it in place instead of spawning a new
|
|
818
|
+
// thread. `resume`, by contrast, loads a DIFFERENT, already-persisted session
|
|
819
|
+
// that owns its own topic — tear the previous runtime down and start fresh
|
|
820
|
+
// under the resumed id so the daemon attaches to (or recreates) that
|
|
821
|
+
// session's own discovery + topic rather than hijacking this terminal's.
|
|
822
|
+
api.on("session_switch", async (event, ctx) => {
|
|
823
|
+
const newId = sessionId(ctx);
|
|
824
|
+
const prevId = sessionIdFromFile(event.previousSessionFile);
|
|
825
|
+
if (!prevId || prevId === newId) return;
|
|
826
|
+
|
|
827
|
+
if (event.reason === "resume") {
|
|
828
|
+
stopSession(prevId);
|
|
829
|
+
await startSession(ctx);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// `/new` / fork: re-key in place and rename the existing topic.
|
|
834
|
+
if (disabledSessions.delete(prevId)) disabledSessions.add(newId);
|
|
835
|
+
const rt = runtimes.get(prevId);
|
|
836
|
+
if (!rt || runtimes.has(newId)) return;
|
|
837
|
+
runtimes.delete(prevId);
|
|
838
|
+
runtimes.set(newId, rt);
|
|
839
|
+
// Re-bind the interactive ask answer source: the ask tool resolves the
|
|
840
|
+
// source by the current session id, which just changed.
|
|
841
|
+
try {
|
|
842
|
+
rt.disposeAnswerSource();
|
|
843
|
+
rt.disposeFileSink();
|
|
844
|
+
} catch {}
|
|
845
|
+
rt.disposeAnswerSource = registerInteractiveAnswerSource(
|
|
846
|
+
newId,
|
|
847
|
+
rt.server,
|
|
848
|
+
rt.pendingInteractive,
|
|
849
|
+
() => rt.redact,
|
|
850
|
+
rt.sessionTag,
|
|
851
|
+
);
|
|
852
|
+
rt.disposeFileSink = registerTelegramFileSink(newId, async file => {
|
|
853
|
+
try {
|
|
854
|
+
const data = await fs.promises.readFile(file.path);
|
|
855
|
+
rt.server.pushFrame(
|
|
856
|
+
JSON.stringify({
|
|
857
|
+
type: "file_attachment",
|
|
858
|
+
sessionId: newId,
|
|
859
|
+
name: path.basename(file.path),
|
|
860
|
+
data: data.toString("base64"),
|
|
861
|
+
caption: file.caption,
|
|
862
|
+
}),
|
|
863
|
+
);
|
|
864
|
+
return { ok: true };
|
|
865
|
+
} catch (e) {
|
|
866
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
// Rename the existing topic now when the new session already has a name; a
|
|
870
|
+
// fresh unnamed session is renamed on its next agent_end re-assert, which
|
|
871
|
+
// avoids a transient rename to bare "repo/branch".
|
|
872
|
+
if (ctx.sessionManager.getSessionName()) {
|
|
873
|
+
try {
|
|
874
|
+
rt.server.pushFrame(
|
|
875
|
+
JSON.stringify({
|
|
876
|
+
type: "identity_header",
|
|
877
|
+
sessionId: newId,
|
|
878
|
+
...buildIdentity(ctx.cwd, ctx.sessionManager.getSessionName()),
|
|
879
|
+
}),
|
|
880
|
+
);
|
|
881
|
+
} catch (e) {
|
|
882
|
+
logger.warn(`notifications: identity_header (switch) failed: ${String(e)}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
515
887
|
// Drive the live typing indicator: mark busy when the agent loop starts so
|
|
516
888
|
// the daemon shows "typing…" in the thread while the agent is thinking,
|
|
517
889
|
// before any turn output exists. Cleared on `agent_end` below.
|
|
@@ -593,7 +965,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
593
965
|
// On idle, stream a context update with metadata (token/model usage +
|
|
594
966
|
// working-tree diff) unless redaction is on. The agent's last message is
|
|
595
967
|
// NOT repeated here — it is already streamed once via `turn_stream`.
|
|
596
|
-
if (!rt.redact) {
|
|
968
|
+
if (!rt.redact && rt.verbosity === "verbose") {
|
|
597
969
|
const usage = (
|
|
598
970
|
ctx as { getContextUsage?: () => { tokens: number | null; contextWindow: number } | undefined }
|
|
599
971
|
).getContextUsage?.();
|
|
@@ -694,7 +1066,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
694
1066
|
}
|
|
695
1067
|
});
|
|
696
1068
|
|
|
697
|
-
api.on("session_shutdown", (_event, ctx) => {
|
|
698
|
-
stopSession(sessionId(ctx));
|
|
1069
|
+
api.on("session_shutdown", async (_event, ctx) => {
|
|
1070
|
+
await stopSession(sessionId(ctx));
|
|
699
1071
|
});
|
|
700
1072
|
};
|