@sayknow-cli/coding-agent 0.3.5 → 0.3.7
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/README.md +76 -0
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/telegram-cli.d.ts +13 -0
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/commands/telegram.d.ts +22 -0
- package/dist/types/config/keybindings.d.ts +12 -6
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +284 -4
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/config/telegram-autostart.d.ts +14 -0
- package/dist/types/config/telegram-env-bridge.d.ts +18 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/defaults/skc-defaults.d.ts +7 -0
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/i18n/messages/en.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -1
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +5 -1
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +15 -3
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +6 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/types.d.ts +5 -1
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +4 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
- package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/skc-runtime/state-writer.d.ts +1 -0
- package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/tools/skill.d.ts +1 -1
- package/dist/types/utils/cmux-workspace.d.ts +52 -0
- package/dist/types/web/search/index.d.ts +4 -2
- package/dist/types/web/search/provider.d.ts +14 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/dist/types/web/search/render.d.ts +1 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +3 -0
- package/scripts/verify-insane-vendor.ts +0 -5
- package/src/capability/mcp.ts +2 -0
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/telegram-cli.ts +156 -0
- package/src/cli/update-cli.ts +179 -13
- package/src/cli/web-search-cli.ts +5 -7
- package/src/cli.ts +20 -1
- package/src/commands/launch.ts +47 -2
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/commands/ralplan.ts +1 -0
- package/src/commands/telegram.ts +47 -0
- package/src/config/keybindings.ts +15 -6
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +9 -0
- package/src/config/settings-schema.ts +256 -4
- package/src/config/settings.ts +11 -0
- package/src/config/telegram-autostart.ts +107 -0
- package/src/config/telegram-env-bridge.ts +92 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
- package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
- package/src/defaults/skc-defaults.ts +16 -0
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +30 -2
- package/src/hooks/skill-state.ts +33 -73
- package/src/i18n/messages/de.ts +1 -0
- package/src/i18n/messages/en.ts +1 -0
- package/src/i18n/messages/es.ts +1 -0
- package/src/i18n/messages/fr.ts +1 -0
- package/src/i18n/messages/ja.settings.ts +28 -0
- package/src/i18n/messages/ja.ts +1 -0
- package/src/i18n/messages/ko.settings.ts +28 -0
- package/src/i18n/messages/ko.ts +1 -0
- package/src/i18n/messages/zh.settings.ts +28 -0
- package/src/i18n/messages/zh.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +12 -7
- package/src/main.ts +5 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/components/agent-dashboard.ts +3 -35
- package/src/modes/components/custom-editor.ts +26 -0
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +246 -11
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +551 -11
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +16 -19
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +281 -117
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +165 -30
- package/src/modes/controllers/command-controller.ts +26 -2
- package/src/modes/controllers/event-controller.ts +102 -4
- package/src/modes/controllers/extension-ui-controller.ts +21 -2
- package/src/modes/controllers/input-controller.ts +214 -47
- package/src/modes/controllers/selector-controller.ts +35 -33
- package/src/modes/interactive-mode.ts +36 -30
- package/src/modes/prompt-action-autocomplete.ts +48 -8
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/theme/theme.ts +5 -1
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/types.ts +8 -1
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +32 -2
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-cli.ts +125 -13
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +88 -46
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/agents/architect.md +7 -4
- package/src/prompts/agents/critic.md +15 -6
- package/src/prompts/agents/planner.md +8 -3
- package/src/prompts/system/system-prompt.md +20 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +10 -11
- package/src/session/agent-session.ts +103 -13
- package/src/session/session-manager.ts +58 -18
- package/src/skc-runtime/launch-tmux.ts +34 -8
- package/src/skc-runtime/launch-worktree.ts +16 -2
- package/src/skc-runtime/ralplan-runtime.ts +22 -6
- package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
- package/src/skc-runtime/session-state-sidecar.ts +167 -24
- package/src/skc-runtime/state-runtime.ts +8 -5
- package/src/skc-runtime/state-writer.ts +5 -5
- package/src/skc-runtime/team-runtime.ts +21 -6
- package/src/skc-runtime/tmux-common.ts +16 -0
- package/src/skc-runtime/tmux-sessions.ts +13 -9
- package/src/skc-runtime/ultragoal-guard.ts +150 -24
- package/src/skc-runtime/ultragoal-runtime.ts +16 -124
- package/src/skc-runtime/workflow-manifest.generated.json +0 -8
- package/src/skc-runtime/workflow-manifest.ts +0 -1
- package/src/slash-commands/builtin-registry.ts +136 -23
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +39 -14
- package/src/tools/index.ts +8 -1
- package/src/tools/skill.ts +1 -1
- package/src/utils/cmux-workspace.ts +233 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +137 -52
- package/src/web/search/provider.ts +80 -0
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/brave.ts +1 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/gemini.ts +7 -3
- package/src/web/search/providers/jina.ts +1 -1
- package/src/web/search/providers/kimi.ts +6 -1
- package/src/web/search/providers/openai-compatible.ts +1 -1
- package/src/web/search/providers/parallel.ts +1 -1
- package/src/web/search/providers/perplexity.ts +2 -2
- package/src/web/search/providers/searxng.ts +1 -1
- package/src/web/search/providers/synthetic.ts +1 -1
- package/src/web/search/providers/tavily.ts +1 -1
- package/src/web/search/providers/utils.ts +67 -17
- package/src/web/search/providers/xai.ts +1 -1
- package/src/web/search/providers/zai.ts +1 -1
- package/src/web/search/render.ts +3 -0
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/vendor/insane-search/MANIFEST.json +1 -3
- package/vendor/insane-search/engine/tests/test_u1.py +0 -28
- package/vendor/insane-search/engine/transport.py +2 -3
- package/vendor/insane-search/engine/validators.py +0 -14
- package/src/defaults/skc/rules/ponytail.md +0 -68
|
@@ -3,6 +3,8 @@ 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
5
|
import { tMessage } from "../../i18n";
|
|
6
|
+
import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/skc-plugins";
|
|
7
|
+
import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
|
|
6
8
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
7
9
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
8
10
|
import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
|
|
@@ -616,9 +618,80 @@ export class UiHelpers {
|
|
|
616
618
|
this.ctx.updatePendingMessagesDisplay();
|
|
617
619
|
this.ctx.showStatus("Queued message for after compaction");
|
|
618
620
|
}
|
|
621
|
+
#hasSkillInvocations(text: string): boolean {
|
|
622
|
+
return parseSkillInvocations(text, this.ctx.skillCommands ?? new Map()).length > 0;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#isCompactionCommandMessage(text: string): boolean {
|
|
626
|
+
return this.#hasSkillInvocations(text) || this.isKnownSlashCommand(text);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async #deliverQueuedSkillMessage(message: CompactionQueuedMessage): Promise<boolean> {
|
|
630
|
+
const invocations = parseSkillInvocations(message.text, this.ctx.skillCommands ?? new Map());
|
|
631
|
+
if (invocations.length === 0) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
for (let index = 0; index < invocations.length; index += 1) {
|
|
636
|
+
const invocation = invocations[index];
|
|
637
|
+
if (!invocation) continue;
|
|
638
|
+
|
|
639
|
+
const activationResult = await resolveSubskillActivationForSkillInvocation({
|
|
640
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
641
|
+
sessionId: this.ctx.session.sessionId,
|
|
642
|
+
skillName: invocation.skill.name,
|
|
643
|
+
args: invocation.args,
|
|
644
|
+
});
|
|
645
|
+
const built = await buildSkillPromptMessage(invocation.skill, activationResult.cleanedArgs, {
|
|
646
|
+
subskillActivation: activationResult.activation,
|
|
647
|
+
subskillActivationSet: activationResult.activeSubskillsToPersist,
|
|
648
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
649
|
+
sessionId: this.ctx.session.sessionId,
|
|
650
|
+
});
|
|
651
|
+
const details: SkillPromptDetails = built.details;
|
|
652
|
+
const displayText = `/${invocation.commandName}${activationResult.cleanedArgs ? ` ${activationResult.cleanedArgs}` : ""}`;
|
|
653
|
+
|
|
654
|
+
if (this.ctx.session.isStreaming) {
|
|
655
|
+
const tag = this.ctx.session.enqueueCustomMessageDisplay(displayText, message.mode);
|
|
656
|
+
details.__pendingDisplayTag = tag;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const isLast = index === invocations.length - 1;
|
|
660
|
+
if (!this.ctx.session.isStreaming && !isLast) {
|
|
661
|
+
await this.ctx.session.sendCustomMessage({
|
|
662
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
663
|
+
content: built.message,
|
|
664
|
+
display: true,
|
|
665
|
+
details,
|
|
666
|
+
attribution: "user",
|
|
667
|
+
});
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
619
670
|
|
|
671
|
+
await this.ctx.session.promptCustomMessage(
|
|
672
|
+
{
|
|
673
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
674
|
+
content: built.message,
|
|
675
|
+
display: true,
|
|
676
|
+
details,
|
|
677
|
+
attribution: "user",
|
|
678
|
+
},
|
|
679
|
+
{ streamingBehavior: message.mode },
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (this.ctx.session.isStreaming) {
|
|
684
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
685
|
+
this.ctx.ui.requestRender();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
620
690
|
async #deliverQueuedMessage(message: CompactionQueuedMessage): Promise<void> {
|
|
621
|
-
if (this
|
|
691
|
+
if (await this.#deliverQueuedSkillMessage(message)) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (this.isKnownSlashCommand(message.text)) {
|
|
622
695
|
await this.ctx.session.prompt(message.text);
|
|
623
696
|
return;
|
|
624
697
|
}
|
|
@@ -677,14 +750,14 @@ export class UiHelpers {
|
|
|
677
750
|
|
|
678
751
|
let firstPromptIndex = -1;
|
|
679
752
|
for (let i = 0; i < queuedMessages.length; i++) {
|
|
680
|
-
if (!this
|
|
753
|
+
if (!this.#isCompactionCommandMessage(queuedMessages[i].text)) {
|
|
681
754
|
firstPromptIndex = i;
|
|
682
755
|
break;
|
|
683
756
|
}
|
|
684
757
|
}
|
|
685
758
|
if (firstPromptIndex === -1) {
|
|
686
759
|
for (const message of queuedMessages) {
|
|
687
|
-
await this
|
|
760
|
+
await this.#deliverQueuedMessage(message);
|
|
688
761
|
}
|
|
689
762
|
return;
|
|
690
763
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface DaemonPaths {
|
|
4
|
+
dir: string;
|
|
5
|
+
lock: string;
|
|
6
|
+
state: string;
|
|
7
|
+
roots: string;
|
|
8
|
+
steal: string;
|
|
9
|
+
aliases: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function daemonPaths(agentDir: string): DaemonPaths {
|
|
13
|
+
const dir = path.join(agentDir, "notifications");
|
|
14
|
+
return {
|
|
15
|
+
dir,
|
|
16
|
+
lock: path.join(dir, "telegram-daemon.lock"),
|
|
17
|
+
state: path.join(dir, "telegram-daemon.state.json"),
|
|
18
|
+
roots: path.join(dir, "telegram-daemon.roots.json"),
|
|
19
|
+
steal: path.join(dir, "telegram-daemon.steal"),
|
|
20
|
+
aliases: path.join(dir, "telegram-callback-aliases.json"),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -248,6 +248,8 @@ interface Token {
|
|
|
248
248
|
value: string;
|
|
249
249
|
/** Tag name if this token opens a tag, else undefined. */
|
|
250
250
|
open?: string;
|
|
251
|
+
/** Exact opening tag text, including attributes, for safe chunk reopening. */
|
|
252
|
+
openTag?: string;
|
|
251
253
|
/** Tag name if this token closes a tag, else undefined. */
|
|
252
254
|
close?: string;
|
|
253
255
|
}
|
|
@@ -266,8 +268,10 @@ function tokenize(html: string): Token[] {
|
|
|
266
268
|
const openMatch = /^<([a-z-]+)(?:\s[^>]*)?>$/i.exec(raw);
|
|
267
269
|
const token: Token = { value: raw };
|
|
268
270
|
if (close && ALLOWED_TAGS.has(close[1]!.toLowerCase())) token.close = close[1]!.toLowerCase();
|
|
269
|
-
else if (openMatch && ALLOWED_TAGS.has(openMatch[1]!.toLowerCase()))
|
|
271
|
+
else if (openMatch && ALLOWED_TAGS.has(openMatch[1]!.toLowerCase())) {
|
|
270
272
|
token.open = openMatch[1]!.toLowerCase();
|
|
273
|
+
token.openTag = raw;
|
|
274
|
+
}
|
|
271
275
|
tokens.push(token);
|
|
272
276
|
i = end + 1;
|
|
273
277
|
continue;
|
|
@@ -330,6 +334,76 @@ export function truncateTelegramHtml(message: string, max = TELEGRAM_MESSAGE_LIM
|
|
|
330
334
|
return out + closersFor(stack) + effectiveMarker;
|
|
331
335
|
}
|
|
332
336
|
|
|
337
|
+
interface OpenTag {
|
|
338
|
+
name: string;
|
|
339
|
+
tag: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Split a finished Telegram HTML message into ordered chunks without splitting tags or entities. */
|
|
343
|
+
export function splitTelegramHtml(message: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
|
|
344
|
+
if (message.length <= max) return [message];
|
|
345
|
+
const chunks: string[] = [];
|
|
346
|
+
const tokens = tokenize(message);
|
|
347
|
+
const stack: OpenTag[] = [];
|
|
348
|
+
let out = "";
|
|
349
|
+
let chunkHasBody = false;
|
|
350
|
+
|
|
351
|
+
const closersFor = (s: OpenTag[]): string =>
|
|
352
|
+
s
|
|
353
|
+
.map(t => `</${t.name}>`)
|
|
354
|
+
.reverse()
|
|
355
|
+
.join("");
|
|
356
|
+
const openersFor = (s: OpenTag[]): string => s.map(t => t.tag).join("");
|
|
357
|
+
const minimumChunkLengthFor = (s: OpenTag[]): number => openersFor(s).length + closersFor(s).length + 1;
|
|
358
|
+
const flush = (): void => {
|
|
359
|
+
if (!chunkHasBody) return;
|
|
360
|
+
chunks.push(out + closersFor(stack));
|
|
361
|
+
out = openersFor(stack);
|
|
362
|
+
chunkHasBody = false;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const updateStackForClose = (name: string): boolean => {
|
|
366
|
+
const idx = stack.findLastIndex(t => t.name === name);
|
|
367
|
+
if (idx === -1) return false;
|
|
368
|
+
stack.splice(idx, 1);
|
|
369
|
+
return true;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
for (const token of tokens) {
|
|
373
|
+
if (token.open) {
|
|
374
|
+
const nextStack = [...stack, { name: token.open, tag: token.openTag ?? token.value }];
|
|
375
|
+
if (minimumChunkLengthFor(nextStack) > max) continue;
|
|
376
|
+
if (chunkHasBody && out.length + token.value.length + closersFor(nextStack).length > max) flush();
|
|
377
|
+
if (out.length + token.value.length + closersFor(nextStack).length > max) continue;
|
|
378
|
+
out += token.value;
|
|
379
|
+
chunkHasBody = true;
|
|
380
|
+
stack.push({ name: token.open, tag: token.openTag ?? token.value });
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (token.close) {
|
|
385
|
+
const idx = stack.findLastIndex(t => t.name === token.close);
|
|
386
|
+
if (idx === -1) continue;
|
|
387
|
+
const nextStack = stack.toSpliced(idx, 1);
|
|
388
|
+
if (chunkHasBody && out.length + token.value.length + closersFor(nextStack).length > max) flush();
|
|
389
|
+
if (out.length + token.value.length + closersFor(nextStack).length > max) {
|
|
390
|
+
updateStackForClose(token.close);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
out += token.value;
|
|
394
|
+
chunkHasBody = true;
|
|
395
|
+
updateStackForClose(token.close);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (chunkHasBody && out.length + token.value.length + closersFor(stack).length > max) flush();
|
|
400
|
+
out += token.value;
|
|
401
|
+
chunkHasBody = true;
|
|
402
|
+
}
|
|
403
|
+
if (chunkHasBody) chunks.push(out + closersFor(stack));
|
|
404
|
+
return chunks;
|
|
405
|
+
}
|
|
406
|
+
|
|
333
407
|
/** Finalize an optional message: undefined passthrough, else safe-truncate. */
|
|
334
408
|
export function finalizeTelegramHtml(message?: string): string | undefined {
|
|
335
409
|
if (message === undefined) return undefined;
|
|
@@ -26,7 +26,7 @@ import * as path from "node:path";
|
|
|
26
26
|
import { promisify } from "node:util";
|
|
27
27
|
import type { ImageContent, TextContent } from "@sayknow-cli/ai";
|
|
28
28
|
import { NotificationServer } from "@sayknow-cli/natives";
|
|
29
|
-
import { logger } from "@sayknow-cli/utils";
|
|
29
|
+
import { logger, postmortem } from "@sayknow-cli/utils";
|
|
30
30
|
import { Settings } from "../config/settings";
|
|
31
31
|
import type { ExtensionCommandContext, ExtensionContext, ExtensionFactory } from "../extensibility/extensions";
|
|
32
32
|
import { registerAskAnswerSource } from "../tools/ask-answer-registry";
|
|
@@ -297,6 +297,15 @@ function buildIdentity(
|
|
|
297
297
|
return { repo, branch, machine: os.hostname(), title: sessionName };
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/** Compact cwd label for remote session identity; never emits the full host path by default. */
|
|
301
|
+
function compactCwd(cwd: string): string | undefined {
|
|
302
|
+
const home = os.homedir();
|
|
303
|
+
const resolved = path.resolve(cwd);
|
|
304
|
+
if (resolved === home) return "~";
|
|
305
|
+
const base = path.basename(resolved);
|
|
306
|
+
return base || path.parse(resolved).root || undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
300
309
|
const execFileAsync = promisify(execFile);
|
|
301
310
|
|
|
302
311
|
/** Best-effort working-tree diff stat for the context update (no throw). */
|
|
@@ -339,6 +348,8 @@ interface SessionRuntime {
|
|
|
339
348
|
/** Assistant text already flushed before an ask this turn (turn-scoped dedupe
|
|
340
349
|
* so turn_end does not re-emit the pre-ask lead-in). Reset each turn. */
|
|
341
350
|
preAskFlushedText?: string;
|
|
351
|
+
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
352
|
+
cancelPostmortemCleanup: () => void;
|
|
342
353
|
}
|
|
343
354
|
|
|
344
355
|
interface ResolvedSettings {
|
|
@@ -491,6 +502,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
491
502
|
if (!rt) return false;
|
|
492
503
|
runtimes.delete(id);
|
|
493
504
|
try {
|
|
505
|
+
rt.cancelPostmortemCleanup();
|
|
494
506
|
rt.disposeAnswerSource();
|
|
495
507
|
rt.disposeFileSink();
|
|
496
508
|
} catch {}
|
|
@@ -671,6 +683,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
671
683
|
pendingInteractive,
|
|
672
684
|
disposeAnswerSource,
|
|
673
685
|
disposeFileSink,
|
|
686
|
+
cancelPostmortemCleanup: () => {},
|
|
674
687
|
redact,
|
|
675
688
|
verbosity,
|
|
676
689
|
sessionTag: tag,
|
|
@@ -678,6 +691,15 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
678
691
|
pendingInbound: new Set<number>(),
|
|
679
692
|
};
|
|
680
693
|
runtimes.set(id, runtime);
|
|
694
|
+
// A native terminal close (SIGHUP), SIGTERM, Ctrl+C exit, or fatal error
|
|
695
|
+
// skips AgentSession.dispose(), so the `session_shutdown` extension event
|
|
696
|
+
// never fires and the daemon-side topic would be orphaned. postmortem
|
|
697
|
+
// awaits registered cleanups on those paths, so send the graceful
|
|
698
|
+
// `session_closed` frame from there too. stopSession() cancels this
|
|
699
|
+
// registration on every other teardown path, so it never double-fires.
|
|
700
|
+
runtime.cancelPostmortemCleanup = postmortem.register(`notifications-session-closed:${id}`, async () => {
|
|
701
|
+
await stopSession(id);
|
|
702
|
+
});
|
|
681
703
|
logger.info(`notifications: serving session ${id} at ${endpoint.url} (unattended=${unattended})`);
|
|
682
704
|
|
|
683
705
|
if (settingsAvailable && settings && isGloballyConfigured(cfg)) {
|
|
@@ -839,9 +861,15 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
839
861
|
// Re-bind the interactive ask answer source: the ask tool resolves the
|
|
840
862
|
// source by the current session id, which just changed.
|
|
841
863
|
try {
|
|
864
|
+
rt.cancelPostmortemCleanup();
|
|
842
865
|
rt.disposeAnswerSource();
|
|
843
866
|
rt.disposeFileSink();
|
|
844
867
|
} catch {}
|
|
868
|
+
// Follow the id change so a later process teardown closes the re-keyed
|
|
869
|
+
// session (the old closure captured the retired id).
|
|
870
|
+
rt.cancelPostmortemCleanup = postmortem.register(`notifications-session-closed:${newId}`, async () => {
|
|
871
|
+
await stopSession(newId);
|
|
872
|
+
});
|
|
845
873
|
rt.disposeAnswerSource = registerInteractiveAnswerSource(
|
|
846
874
|
newId,
|
|
847
875
|
rt.server,
|
|
@@ -973,7 +1001,8 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
973
1001
|
const tokenUsage = usage && usage.tokens != null ? `${usage.tokens}/${usage.contextWindow}` : undefined;
|
|
974
1002
|
const modelId = model?.id;
|
|
975
1003
|
void readGitDiffStat(ctx.cwd).then(diff => {
|
|
976
|
-
|
|
1004
|
+
const cwd = compactCwd(ctx.cwd);
|
|
1005
|
+
if (!diff && !tokenUsage && !modelId && !cwd) return;
|
|
977
1006
|
try {
|
|
978
1007
|
rt.server.pushFrame(
|
|
979
1008
|
JSON.stringify({
|
|
@@ -982,6 +1011,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
982
1011
|
tokenUsage,
|
|
983
1012
|
model: modelId,
|
|
984
1013
|
diff,
|
|
1014
|
+
cwd,
|
|
985
1015
|
}),
|
|
986
1016
|
);
|
|
987
1017
|
} catch (e) {
|
|
@@ -238,6 +238,7 @@ export function daemonResumeSession(env: NodeJS.ProcessEnv = process.env, opts:
|
|
|
238
238
|
// restart, so an unknown or ambiguous prefix fails closed instead of
|
|
239
239
|
// blindly spawning `skc --resume <prefix>` against a non-authoritative id.
|
|
240
240
|
let resumeId = target.sessionIdOrPrefix;
|
|
241
|
+
let resumeCwd = target.path;
|
|
241
242
|
if (opts.sessionsRoot) {
|
|
242
243
|
const saved = listRecentSessions({ sessionsRoot: opts.sessionsRoot, limit: 1000 });
|
|
243
244
|
const prefixed = saved.filter(
|
|
@@ -249,11 +250,19 @@ export function daemonResumeSession(env: NodeJS.ProcessEnv = process.env, opts:
|
|
|
249
250
|
if (resolved.length > 1) {
|
|
250
251
|
return { ambiguous: resolved.map(s => ({ sessionId: s.sessionId, path: s.path })) };
|
|
251
252
|
}
|
|
252
|
-
|
|
253
|
+
const selected = resolved[0]!;
|
|
254
|
+
resumeId = selected.sessionId;
|
|
255
|
+
resumeCwd = selected.path;
|
|
256
|
+
}
|
|
257
|
+
const resolvedResumeCwd = resumeCwd ? path.resolve(resumeCwd) : undefined;
|
|
258
|
+
const resumeCwdStat = resolvedResumeCwd ? fs.statSync(resolvedResumeCwd, { throwIfNoEntry: false }) : undefined;
|
|
259
|
+
if (!resumeCwdStat?.isDirectory()) {
|
|
260
|
+
throw new Error(`skc_lifecycle_resume_cwd_unavailable: ${resolvedResumeCwd ?? "(missing)"}`);
|
|
253
261
|
}
|
|
254
262
|
const tmux = resolveSkcTmuxCommand(env);
|
|
255
263
|
const name = tmuxSessionNameFor(resumeId);
|
|
256
|
-
const
|
|
264
|
+
const cwdPrefix = resolvedResumeCwd ? `cd ${shellQuote(resolvedResumeCwd)} && ` : "";
|
|
265
|
+
const command = `${cwdPrefix}exec env SKC_TMUX_LAUNCHED=1 SKC_NOTIFICATIONS=1 skc --resume ${shellQuote(resumeId)}`;
|
|
257
266
|
const r = Bun.spawnSync([tmux, "new-session", "-d", "-s", name, "sh", "-c", command], {
|
|
258
267
|
stdout: "pipe",
|
|
259
268
|
stderr: "pipe",
|
|
@@ -261,7 +270,7 @@ export function daemonResumeSession(env: NodeJS.ProcessEnv = process.env, opts:
|
|
|
261
270
|
});
|
|
262
271
|
if (r.exitCode !== 0) throw new Error(r.stderr.toString().trim() || "skc_lifecycle_resume_failed");
|
|
263
272
|
const tgt = buildSkcTmuxExactOptionTarget(name);
|
|
264
|
-
for (const cmd of buildSkcTmuxProfileCommands(tgt, env, { sessionId: resumeId })) {
|
|
273
|
+
for (const cmd of buildSkcTmuxProfileCommands(tgt, env, { sessionId: resumeId, project: resolvedResumeCwd })) {
|
|
265
274
|
Bun.spawnSync([tmux, ...cmd.args], { stdout: "pipe", stderr: "pipe", env });
|
|
266
275
|
}
|
|
267
276
|
return {
|
|
@@ -25,6 +25,8 @@ export interface RecentSessionEntry {
|
|
|
25
25
|
mtimeMs: number;
|
|
26
26
|
/** True when a terminal breadcrumb points at this session file. */
|
|
27
27
|
currentTerminal?: boolean;
|
|
28
|
+
/** True when this history is an internal helper/sub-agent session. */
|
|
29
|
+
internal?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface RecentActivityDeps {
|
|
@@ -34,17 +36,18 @@ export interface RecentActivityDeps {
|
|
|
34
36
|
breadcrumbPaths?: string[];
|
|
35
37
|
/** Max entries to return (default 20). */
|
|
36
38
|
limit?: number;
|
|
39
|
+
/** Include internal helper/sub-agent sessions (default true). */
|
|
40
|
+
includeInternal?: boolean;
|
|
37
41
|
/** Injection seam for tests. */
|
|
38
|
-
|
|
42
|
+
readInitialLines?: (file: string, maxLines: number) => string[];
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
function
|
|
45
|
+
function defaultReadInitialLines(file: string, maxLines: number): string[] {
|
|
42
46
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
return nl === -1 ? buf : buf.slice(0, nl);
|
|
47
|
+
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
48
|
+
return lines.slice(0, maxLines);
|
|
46
49
|
} catch {
|
|
47
|
-
return
|
|
50
|
+
return [];
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -65,6 +68,22 @@ function headerMeta(line: string | undefined): { id?: string; path?: string; bra
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/** Detect task-tool helper sessions from the durable early session_init metadata entry. */
|
|
72
|
+
function isInternalSession(lines: readonly string[]): boolean {
|
|
73
|
+
for (const line of lines.slice(1)) {
|
|
74
|
+
if (!line.trim()) continue;
|
|
75
|
+
try {
|
|
76
|
+
const obj = JSON.parse(line) as unknown;
|
|
77
|
+
if (typeof obj === "object" && obj !== null && (obj as { type?: unknown }).type === "session_init") {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore malformed JSONL entries; classification is best-effort.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
/**
|
|
69
88
|
* The authoritative session id for a history file: the header `id` when present,
|
|
70
89
|
* else the filename stem with a leading `<timestamp>_` prefix stripped (matching
|
|
@@ -85,7 +104,8 @@ function sessionIdForFile(stem: string, headerId: string | undefined): string {
|
|
|
85
104
|
*/
|
|
86
105
|
export function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry[] {
|
|
87
106
|
const limit = deps.limit ?? 20;
|
|
88
|
-
const
|
|
107
|
+
const includeInternal = deps.includeInternal ?? true;
|
|
108
|
+
const readInitialLines = deps.readInitialLines ?? defaultReadInitialLines;
|
|
89
109
|
const breadcrumbs = new Set((deps.breadcrumbPaths ?? []).map(p => path.resolve(p)));
|
|
90
110
|
|
|
91
111
|
let projectDirs: string[];
|
|
@@ -114,7 +134,10 @@ export function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry
|
|
|
114
134
|
} catch {
|
|
115
135
|
continue;
|
|
116
136
|
}
|
|
117
|
-
const
|
|
137
|
+
const initialLines = readInitialLines(file, 8);
|
|
138
|
+
const meta = headerMeta(initialLines[0]);
|
|
139
|
+
const internal = isInternalSession(initialLines);
|
|
140
|
+
if (internal && !includeInternal) continue;
|
|
118
141
|
entries.push({
|
|
119
142
|
sessionId: sessionIdForFile(name.slice(0, -".jsonl".length), meta.id),
|
|
120
143
|
path: meta.path,
|
|
@@ -123,6 +146,7 @@ export function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry
|
|
|
123
146
|
sessionStateFile: file,
|
|
124
147
|
mtimeMs,
|
|
125
148
|
currentTerminal: breadcrumbs.has(path.resolve(file)) || undefined,
|
|
149
|
+
internal: internal || undefined,
|
|
126
150
|
});
|
|
127
151
|
}
|
|
128
152
|
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { YAML } from "bun";
|
|
4
|
+
import type { Settings } from "../config/settings";
|
|
4
5
|
import { getNotificationConfig, isGloballyConfigured } from "./config";
|
|
5
|
-
import { daemonPaths
|
|
6
|
-
import {
|
|
6
|
+
import { daemonPaths } from "./daemon-paths";
|
|
7
|
+
import type { TelegramDaemonOptions } from "./telegram-daemon";
|
|
8
|
+
|
|
9
|
+
type TelegramDaemonRunner = {
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
requestStop(reason?: "reload" | "signal" | "stop"): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type TelegramDaemonConstructor = new (opts: TelegramDaemonOptions) => TelegramDaemonRunner;
|
|
7
15
|
|
|
8
16
|
export interface RunDaemonInternalDeps {
|
|
9
|
-
SettingsImpl?: Pick<
|
|
10
|
-
DaemonImpl?:
|
|
17
|
+
SettingsImpl?: { init: (options?: { agentDir?: string }) => Promise<Pick<Settings, "get" | "getAgentDir">> };
|
|
18
|
+
DaemonImpl?: TelegramDaemonConstructor;
|
|
11
19
|
processPid?: number;
|
|
20
|
+
pidAlive?: (pid: number) => boolean;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
function argValue(argv: string[], name: string): string | undefined {
|
|
@@ -16,9 +25,105 @@ function argValue(argv: string[], name: string): string | undefined {
|
|
|
16
25
|
return i >= 0 ? argv[i + 1] : undefined;
|
|
17
26
|
}
|
|
18
27
|
|
|
28
|
+
function getByPath(obj: unknown, pathSegments: string[]): unknown {
|
|
29
|
+
let current = obj;
|
|
30
|
+
for (const segment of pathSegments) {
|
|
31
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return undefined;
|
|
32
|
+
current = (current as Record<string, unknown>)[segment];
|
|
33
|
+
}
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function asString(value: unknown): string | undefined {
|
|
38
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function asBoolean(value: unknown, fallback: boolean): boolean {
|
|
42
|
+
return typeof value === "boolean" ? value : fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function asIdleTimeoutMs(value: unknown): number {
|
|
46
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 60_000;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createLightweightDaemonSettings(input: {
|
|
50
|
+
agentDir: string;
|
|
51
|
+
rawConfig?: unknown;
|
|
52
|
+
}): Pick<Settings, "get" | "getAgentDir"> {
|
|
53
|
+
const rawConfig = input.rawConfig && typeof input.rawConfig === "object" ? input.rawConfig : {};
|
|
54
|
+
return {
|
|
55
|
+
get(pathName: string): unknown {
|
|
56
|
+
const value = getByPath(rawConfig, pathName.split("."));
|
|
57
|
+
switch (pathName) {
|
|
58
|
+
case "notifications.enabled":
|
|
59
|
+
return asBoolean(value, false);
|
|
60
|
+
case "notifications.telegram.botToken":
|
|
61
|
+
case "notifications.telegram.chatId":
|
|
62
|
+
case "notifications.discord.botToken":
|
|
63
|
+
case "notifications.discord.channelId":
|
|
64
|
+
case "notifications.slack.botToken":
|
|
65
|
+
case "notifications.slack.channelId":
|
|
66
|
+
return asString(value);
|
|
67
|
+
case "notifications.redact":
|
|
68
|
+
return asBoolean(value, false);
|
|
69
|
+
case "notifications.verbosity":
|
|
70
|
+
return value === "verbose" ? "verbose" : "lean";
|
|
71
|
+
case "notifications.daemon.idleTimeoutMs":
|
|
72
|
+
return asIdleTimeoutMs(value);
|
|
73
|
+
default:
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
getAgentDir(): string {
|
|
78
|
+
return input.agentDir;
|
|
79
|
+
},
|
|
80
|
+
} as Pick<Settings, "get" | "getAgentDir">;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function loadLightweightDaemonSettings(agentDir: string): Promise<Pick<Settings, "get" | "getAgentDir">> {
|
|
84
|
+
const configPath = path.join(agentDir, "config.yml");
|
|
85
|
+
let rawConfig: unknown = {};
|
|
86
|
+
try {
|
|
87
|
+
rawConfig = YAML.parse(await fs.promises.readFile(configPath, "utf8"));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
90
|
+
}
|
|
91
|
+
return createLightweightDaemonSettings({ agentDir, rawConfig });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function resolveDaemonSettings(
|
|
95
|
+
agentDir: string,
|
|
96
|
+
deps: RunDaemonInternalDeps,
|
|
97
|
+
): Promise<Pick<Settings, "get" | "getAgentDir">> {
|
|
98
|
+
if (deps.SettingsImpl) return await deps.SettingsImpl.init({ agentDir });
|
|
99
|
+
return await loadLightweightDaemonSettings(agentDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function ownerPidFromOwnerId(ownerId: string): number | undefined {
|
|
103
|
+
const match = /^(\d+)(?:-|$)/.exec(ownerId);
|
|
104
|
+
if (!match) return undefined;
|
|
105
|
+
const pid = Number(match[1]);
|
|
106
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function defaultPidAlive(pid: number): boolean {
|
|
110
|
+
try {
|
|
111
|
+
process.kill(pid, 0);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ownerProcessIsAlive(ownerId: string, deps: RunDaemonInternalDeps): boolean {
|
|
119
|
+
const ownerPid = ownerPidFromOwnerId(ownerId);
|
|
120
|
+
if (ownerPid === undefined) return true;
|
|
121
|
+
return (deps.pidAlive ?? defaultPidAlive)(ownerPid);
|
|
122
|
+
}
|
|
123
|
+
|
|
19
124
|
export async function runDaemonSmoke(opts: { agentDir?: string } = {}): Promise<void> {
|
|
20
125
|
const agentDir = opts.agentDir ?? fs.mkdtempSync(path.join(process.cwd(), ".telegram-daemon-smoke-"));
|
|
21
|
-
const settings =
|
|
126
|
+
const settings = createLightweightDaemonSettings({ agentDir, rawConfig: {} });
|
|
22
127
|
const paths = daemonPaths(agentDir);
|
|
23
128
|
await fs.promises.mkdir(paths.dir, { recursive: true, mode: 0o700 });
|
|
24
129
|
const tempLock = `${paths.lock}.smoke.${process.pid}`;
|
|
@@ -34,12 +139,19 @@ export async function runDaemonInternal(argv: string[], deps: RunDaemonInternalD
|
|
|
34
139
|
if (smoke) return runDaemonSmoke({ agentDir });
|
|
35
140
|
const ownerId = argValue(argv, "--owner-id");
|
|
36
141
|
if (!ownerId) throw new Error("missing --owner-id");
|
|
37
|
-
|
|
38
|
-
|
|
142
|
+
if (!ownerProcessIsAlive(ownerId, deps)) {
|
|
143
|
+
process.stderr.write(`SKC notify daemon exiting: owner process from --owner-id ${ownerId} is not alive.\n`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const resolvedAgentDir = agentDir ?? process.env.SKC_CODING_AGENT_DIR ?? path.join(process.cwd(), ".skc", "agent");
|
|
147
|
+
const settings = await resolveDaemonSettings(resolvedAgentDir, deps);
|
|
148
|
+
const cfg = getNotificationConfig(settings as Settings);
|
|
39
149
|
if (!isGloballyConfigured(cfg) || !cfg.botToken || !cfg.chatId) return;
|
|
40
|
-
const
|
|
150
|
+
const { clearTelegramControlRequest, readTelegramControlRequest } = await import("./telegram-daemon-control");
|
|
151
|
+
const Daemon: TelegramDaemonConstructor =
|
|
152
|
+
deps.DaemonImpl ?? (await import("./telegram-daemon")).TelegramNotificationDaemon;
|
|
41
153
|
const daemon = new Daemon({
|
|
42
|
-
settings,
|
|
154
|
+
settings: settings as Settings,
|
|
43
155
|
ownerId,
|
|
44
156
|
botToken: cfg.botToken,
|
|
45
157
|
chatId: cfg.chatId,
|
|
@@ -47,15 +159,15 @@ export async function runDaemonInternal(argv: string[], deps: RunDaemonInternalD
|
|
|
47
159
|
pid: deps.processPid ?? process.pid,
|
|
48
160
|
control: {
|
|
49
161
|
shouldStop: async owner => {
|
|
50
|
-
const req = await readTelegramControlRequest(settings);
|
|
162
|
+
const req = await readTelegramControlRequest(settings as Settings);
|
|
51
163
|
return Boolean(req && (!req.ownerId || req.ownerId === owner));
|
|
52
164
|
},
|
|
53
165
|
clear: async owner => {
|
|
54
|
-
const req = await readTelegramControlRequest(settings);
|
|
166
|
+
const req = await readTelegramControlRequest(settings as Settings);
|
|
55
167
|
// Only clear a request that targets this daemon owner, so an exiting
|
|
56
168
|
// daemon never erases a newer request meant for a different owner.
|
|
57
169
|
if (req && (!req.ownerId || req.ownerId === owner)) {
|
|
58
|
-
await clearTelegramControlRequest(settings, req.requestId);
|
|
170
|
+
await clearTelegramControlRequest(settings as Settings, req.requestId);
|
|
59
171
|
}
|
|
60
172
|
},
|
|
61
173
|
},
|
|
@@ -97,6 +97,14 @@ export interface TelegramDaemonControlDeps {
|
|
|
97
97
|
sendSignal?: (pid: number, signal: NodeJS.Signals) => void;
|
|
98
98
|
spawn?: TelegramDaemonDeps["spawn"];
|
|
99
99
|
execPath?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Stable process id encoded into freshly-spawned daemon owner ids.
|
|
102
|
+
*
|
|
103
|
+
* The daemon-internal entrypoint rejects numeric owner ids whose process is
|
|
104
|
+
* already gone. `skc daemon reload` is a short-lived CLI process, so using its
|
|
105
|
+
* own pid can race the child startup and make the replacement exit immediately.
|
|
106
|
+
*/
|
|
107
|
+
ownerPid?: number;
|
|
100
108
|
randomId?: () => string;
|
|
101
109
|
sleep?: (ms: number) => Promise<void>;
|
|
102
110
|
waitStepMs?: number;
|
|
@@ -188,6 +196,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
188
196
|
fs: this.deps.fs,
|
|
189
197
|
now: this.deps.now,
|
|
190
198
|
pidAlive: this.deps.pidAlive,
|
|
199
|
+
pid: this.deps.ownerPid ?? process.ppid,
|
|
191
200
|
spawn: this.deps.spawn,
|
|
192
201
|
execPath: this.deps.execPath,
|
|
193
202
|
randomId: this.deps.randomId,
|