@sayknow-cli/coding-agent 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +105 -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/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/interactive-mode.d.ts +2 -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-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 +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config.d.ts +14 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +23 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/notifications/threaded-render.d.ts +6 -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/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/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 +92 -8
- package/src/cli/telegram-cli.ts +1 -1
- package/src/cli/update-cli.ts +85 -9
- 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 +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-profiles.ts +31 -2
- package/src/config/settings-schema.ts +68 -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 +14 -5
- 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/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/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 +66 -47
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +180 -18
- package/src/modes/controllers/selector-controller.ts +127 -24
- package/src/modes/interactive-mode.ts +51 -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 +29 -3
- 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 +1 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +20 -7
- package/src/notifications/config.ts +25 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +117 -14
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +302 -51
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/notifications/threaded-render.ts +17 -1
- 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 +1 -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 +52 -28
- package/src/session/agent-session.ts +1011 -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/ultragoal-guard.ts +55 -66
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/skc-runtime/ultragoal-runtime.ts +1663 -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 +218 -71
- 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/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
|
@@ -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";
|
|
@@ -100,7 +100,7 @@ export interface SessionCloseFrame {
|
|
|
100
100
|
chatId: string;
|
|
101
101
|
token: string;
|
|
102
102
|
target: SessionCloseTarget;
|
|
103
|
-
/**
|
|
103
|
+
/** Required force-only close flag; false/omitted is rejected by daemon policy. */
|
|
104
104
|
force?: boolean;
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -336,6 +336,8 @@ interface SessionRuntime {
|
|
|
336
336
|
disposeAnswerSource: () => void;
|
|
337
337
|
/** Deregisters this session's Telegram file sink. */
|
|
338
338
|
disposeFileSink: () => void;
|
|
339
|
+
/** Deregisters this session's unattended workflow-gate listener. */
|
|
340
|
+
disposeGateListener: () => void;
|
|
339
341
|
redact: boolean;
|
|
340
342
|
verbosity: "lean" | "verbose";
|
|
341
343
|
sessionTag: string;
|
|
@@ -348,6 +350,12 @@ interface SessionRuntime {
|
|
|
348
350
|
/** Assistant text already flushed before an ask this turn (turn-scoped dedupe
|
|
349
351
|
* so turn_end does not re-emit the pre-ask lead-in). Reset each turn. */
|
|
350
352
|
preAskFlushedText?: string;
|
|
353
|
+
/** Live streaming: opt-in flag, monotonic per-turn ref, and emit throttle state. */
|
|
354
|
+
stream: boolean;
|
|
355
|
+
turnSeq?: number;
|
|
356
|
+
liveRef?: string;
|
|
357
|
+
lastLiveAt?: number;
|
|
358
|
+
lastLiveText?: string;
|
|
351
359
|
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
352
360
|
cancelPostmortemCleanup: () => void;
|
|
353
361
|
}
|
|
@@ -358,6 +366,8 @@ interface ResolvedSettings {
|
|
|
358
366
|
settingsAvailable: boolean;
|
|
359
367
|
}
|
|
360
368
|
|
|
369
|
+
const TELEGRAM_FILE_REDACTION_ERROR = "Telegram file attachments are disabled while notifications redaction is on.";
|
|
370
|
+
|
|
361
371
|
const defaultConfig: NotificationConfig = {
|
|
362
372
|
enabled: false,
|
|
363
373
|
botToken: undefined,
|
|
@@ -379,7 +389,36 @@ export function notificationsEnabled(): boolean {
|
|
|
379
389
|
return process.env.SKC_NOTIFICATIONS === "1" || Boolean(process.env.SKC_NOTIFICATIONS_TOKEN);
|
|
380
390
|
}
|
|
381
391
|
|
|
382
|
-
|
|
392
|
+
// Live streaming (opt-in): emit throttled non-finalized `turn_stream` frames as
|
|
393
|
+
// the assistant message streams so remote clients can edit ONE message live. The
|
|
394
|
+
// finalized frame (turn_end) carries the same messageRef and stays authoritative,
|
|
395
|
+
// so a dropped live frame self-heals. Off unless SKC_NOTIFICATIONS_STREAM=1.
|
|
396
|
+
function streamingEnabled(): boolean {
|
|
397
|
+
return process.env.SKC_NOTIFICATIONS_STREAM === "1";
|
|
398
|
+
}
|
|
399
|
+
function streamIntervalMs(): number {
|
|
400
|
+
return Math.max(200, Number(process.env.SKC_NOTIFICATIONS_STREAM_INTERVAL_MS) || 500);
|
|
401
|
+
}
|
|
402
|
+
// Max chars of a turn's assistant text carried by the FINALIZED turn_stream (and
|
|
403
|
+
// the pre-ask capture). Default 3500 keeps the mirror a glanceable per-turn
|
|
404
|
+
// summary; a client that splits long messages (the Telegram daemon does so via
|
|
405
|
+
// splitTelegramHtml, scheduling each chunk through the shared rate-limit pool so
|
|
406
|
+
// the fan-out never bypasses the per-chat limit) can raise it with
|
|
407
|
+
// SKC_NOTIFICATIONS_TURN_MAX to deliver full turns. The value is clamped to a
|
|
408
|
+
// finite [280, TURN_TEXT_MAX_CEILING] range: a non-finite or non-positive env
|
|
409
|
+
// (unset, NaN, Infinity, <= 0) falls back to the default, so the cap can never
|
|
410
|
+
// be unbounded. Live frames are intentionally NOT raised — they stay one
|
|
411
|
+
// editable preview message rather than fanning a long in-progress turn across
|
|
412
|
+
// sends.
|
|
413
|
+
const TURN_TEXT_MAX_CEILING = 40_000;
|
|
414
|
+
function turnTextMax(): number {
|
|
415
|
+
const raw = Number(process.env.SKC_NOTIFICATIONS_TURN_MAX);
|
|
416
|
+
if (!Number.isFinite(raw) || raw <= 0) return 3500;
|
|
417
|
+
return Math.min(TURN_TEXT_MAX_CEILING, Math.max(280, raw));
|
|
418
|
+
}
|
|
419
|
+
function resolveSettings(settingsOverride?: Settings): ResolvedSettings {
|
|
420
|
+
if (settingsOverride)
|
|
421
|
+
return { settings: settingsOverride, cfg: getNotificationConfig(settingsOverride), settingsAvailable: true };
|
|
383
422
|
try {
|
|
384
423
|
const settings = Settings.instance;
|
|
385
424
|
return { settings, cfg: getNotificationConfig(settings), settingsAvailable: true };
|
|
@@ -491,7 +530,7 @@ function sessionIdFromFile(file: string | undefined): string | undefined {
|
|
|
491
530
|
return underscore >= 0 ? base.slice(underscore + 1) : undefined;
|
|
492
531
|
}
|
|
493
532
|
|
|
494
|
-
export
|
|
533
|
+
export function createNotificationsExtension(api: ExtensionAPI, options: { settings?: Settings } = {}): void {
|
|
495
534
|
const runtimes = new Map<string, SessionRuntime>();
|
|
496
535
|
const disabledSessions = new Set<string>();
|
|
497
536
|
const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
|
|
@@ -503,9 +542,16 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
503
542
|
runtimes.delete(id);
|
|
504
543
|
try {
|
|
505
544
|
rt.cancelPostmortemCleanup();
|
|
545
|
+
} catch {}
|
|
546
|
+
try {
|
|
506
547
|
rt.disposeAnswerSource();
|
|
548
|
+
} catch {}
|
|
549
|
+
try {
|
|
507
550
|
rt.disposeFileSink();
|
|
508
551
|
} catch {}
|
|
552
|
+
try {
|
|
553
|
+
rt.disposeGateListener();
|
|
554
|
+
} catch {}
|
|
509
555
|
// Resolve any still-pending interactive asks so the ask tool is not left hanging.
|
|
510
556
|
for (const pending of rt.pendingInteractive.values()) pending.resolve(undefined);
|
|
511
557
|
rt.pendingInteractive.clear();
|
|
@@ -529,10 +575,14 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
529
575
|
return isSessionNotificationsEnabled({ cfg, env: process.env, sessionDisabled: disabledSessions.has(id) });
|
|
530
576
|
}
|
|
531
577
|
|
|
578
|
+
function isNotificationEligibleContext(ctx: ExtensionContext): boolean {
|
|
579
|
+
return ctx.sessionMetadata?.kind !== "sub";
|
|
580
|
+
}
|
|
581
|
+
|
|
532
582
|
async function startSession(ctx: ExtensionContext): Promise<"started" | "already" | "disabled" | "failed"> {
|
|
533
583
|
const id = sessionId(ctx);
|
|
534
|
-
const { settings, cfg, settingsAvailable } = resolveSettings();
|
|
535
|
-
if (!isEnabledForSession(id, cfg)) return "disabled";
|
|
584
|
+
const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
|
|
585
|
+
if (!isNotificationEligibleContext(ctx) || !isEnabledForSession(id, cfg)) return "disabled";
|
|
536
586
|
if (runtimes.has(id)) return "already";
|
|
537
587
|
|
|
538
588
|
const stateRoot = path.join(ctx.cwd, ".skc", "state");
|
|
@@ -660,6 +710,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
660
710
|
tag,
|
|
661
711
|
);
|
|
662
712
|
const disposeFileSink = registerTelegramFileSink(id, async file => {
|
|
713
|
+
if (runtime?.redact ?? redact) {
|
|
714
|
+
return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
715
|
+
}
|
|
716
|
+
|
|
663
717
|
try {
|
|
664
718
|
const data = await fs.promises.readFile(file.path);
|
|
665
719
|
server.pushFrame(
|
|
@@ -683,9 +737,11 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
683
737
|
pendingInteractive,
|
|
684
738
|
disposeAnswerSource,
|
|
685
739
|
disposeFileSink,
|
|
740
|
+
disposeGateListener: () => {},
|
|
686
741
|
cancelPostmortemCleanup: () => {},
|
|
687
742
|
redact,
|
|
688
743
|
verbosity,
|
|
744
|
+
stream: streamingEnabled(),
|
|
689
745
|
sessionTag: tag,
|
|
690
746
|
busy: false,
|
|
691
747
|
pendingInbound: new Set<number>(),
|
|
@@ -702,7 +758,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
702
758
|
});
|
|
703
759
|
logger.info(`notifications: serving session ${id} at ${endpoint.url} (unattended=${unattended})`);
|
|
704
760
|
|
|
705
|
-
if (settingsAvailable && settings &&
|
|
761
|
+
if (settingsAvailable && settings && isTelegramConfigured(cfg)) {
|
|
706
762
|
try {
|
|
707
763
|
await ensureTelegramDaemonRunning({ settings, cwd: ctx.cwd, sessionId: id });
|
|
708
764
|
} catch (e) {
|
|
@@ -726,7 +782,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
726
782
|
|
|
727
783
|
// Unattended: a real ask emits a workflow gate; register it repliable by gate_id.
|
|
728
784
|
if (unattended && gate?.onGateEmitted) {
|
|
729
|
-
gate.onGateEmitted(g => {
|
|
785
|
+
runtime.disposeGateListener = gate.onGateEmitted(g => {
|
|
730
786
|
const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
|
|
731
787
|
gateOptions.set(g.gate_id, options);
|
|
732
788
|
const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
|
|
@@ -761,7 +817,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
761
817
|
async handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
762
818
|
const id = sessionId(ctx);
|
|
763
819
|
const command = args.trim().split(/\s+/, 1)[0]?.toLowerCase() || "status";
|
|
764
|
-
const resolved = resolveSettings();
|
|
820
|
+
const resolved = resolveSettings(options.settings);
|
|
765
821
|
const enabledWithoutLocalOff = isSessionNotificationsEnabled({
|
|
766
822
|
cfg: resolved.cfg,
|
|
767
823
|
env: process.env,
|
|
@@ -781,6 +837,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
781
837
|
}
|
|
782
838
|
|
|
783
839
|
if (command === "on") {
|
|
840
|
+
if (!isNotificationEligibleContext(ctx)) {
|
|
841
|
+
ctx.ui.notify("Notifications are disabled for subagent sessions.", "warning");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
784
844
|
if (process.env.SKC_NOTIFICATIONS === "0") {
|
|
785
845
|
ctx.ui.notify(
|
|
786
846
|
"Notifications remain disabled: SKC_NOTIFICATIONS=0 is an authoritative opt-out.",
|
|
@@ -878,6 +938,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
878
938
|
rt.sessionTag,
|
|
879
939
|
);
|
|
880
940
|
rt.disposeFileSink = registerTelegramFileSink(newId, async file => {
|
|
941
|
+
if (rt.redact) {
|
|
942
|
+
return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
943
|
+
}
|
|
944
|
+
|
|
881
945
|
try {
|
|
882
946
|
const data = await fs.promises.readFile(file.path);
|
|
883
947
|
rt.server.pushFrame(
|
|
@@ -1033,7 +1097,15 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
1033
1097
|
if (!text || text === rt.preAskFlushedText) return;
|
|
1034
1098
|
rt.preAskFlushedText = text;
|
|
1035
1099
|
try {
|
|
1036
|
-
rt.server.pushFrame(
|
|
1100
|
+
rt.server.pushFrame(
|
|
1101
|
+
JSON.stringify({
|
|
1102
|
+
type: "turn_stream",
|
|
1103
|
+
sessionId: id,
|
|
1104
|
+
phase: "finalized",
|
|
1105
|
+
text,
|
|
1106
|
+
...(rt.liveRef ? { messageRef: rt.liveRef } : {}),
|
|
1107
|
+
}),
|
|
1108
|
+
);
|
|
1037
1109
|
} catch (e) {
|
|
1038
1110
|
logger.warn(`notifications: pushFrame (turn) failed: ${String(e)}`);
|
|
1039
1111
|
}
|
|
@@ -1057,12 +1129,43 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
1057
1129
|
const id = sessionId(ctx);
|
|
1058
1130
|
const rt = runtimes.get(id);
|
|
1059
1131
|
if (!rt) return;
|
|
1060
|
-
const text = rt.redact ? undefined : summaryFromMessage(event.message,
|
|
1132
|
+
const text = rt.redact ? undefined : summaryFromMessage(event.message, turnTextMax());
|
|
1061
1133
|
if (text) flushTurnText(rt, id, text);
|
|
1062
1134
|
// Reset per-turn streaming state so the next turn starts fresh and a later
|
|
1063
1135
|
// turn with identical text is not falsely deduped.
|
|
1064
1136
|
rt.currentTurnText = undefined;
|
|
1065
1137
|
rt.preAskFlushedText = undefined;
|
|
1138
|
+
rt.liveRef = undefined;
|
|
1139
|
+
rt.lastLiveAt = undefined;
|
|
1140
|
+
rt.lastLiveText = undefined;
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
// Live streaming (opt-in): push throttled in-progress assistant text as
|
|
1144
|
+
// non-finalized turn_stream frames so remote clients edit one message as the
|
|
1145
|
+
// turn streams. The finalized frame (turn_end) carries the same messageRef and
|
|
1146
|
+
// lands the authoritative text. Suppressed under redaction.
|
|
1147
|
+
api.on("message_update", (event, ctx) => {
|
|
1148
|
+
const id = sessionId(ctx);
|
|
1149
|
+
const rt = runtimes.get(id);
|
|
1150
|
+
if (!rt?.stream || rt.redact) return;
|
|
1151
|
+
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
1152
|
+
if (rt.liveRef === undefined) {
|
|
1153
|
+
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
1154
|
+
rt.liveRef = String(rt.turnSeq);
|
|
1155
|
+
}
|
|
1156
|
+
const now = Date.now();
|
|
1157
|
+
if (now - (rt.lastLiveAt ?? 0) < streamIntervalMs()) return;
|
|
1158
|
+
const text = summaryFromMessage(event.message, 3500);
|
|
1159
|
+
if (!text || text === rt.lastLiveText) return;
|
|
1160
|
+
rt.lastLiveAt = now;
|
|
1161
|
+
rt.lastLiveText = text;
|
|
1162
|
+
try {
|
|
1163
|
+
rt.server.pushFrame(
|
|
1164
|
+
JSON.stringify({ type: "turn_stream", sessionId: id, phase: "live", text, messageRef: rt.liveRef }),
|
|
1165
|
+
);
|
|
1166
|
+
} catch (e) {
|
|
1167
|
+
logger.warn(`notifications: pushFrame (live) failed: ${String(e)}`);
|
|
1168
|
+
}
|
|
1066
1169
|
});
|
|
1067
1170
|
|
|
1068
1171
|
// Stream agent-produced images (computer/browser/tool screenshots) as
|
|
@@ -1076,7 +1179,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
1076
1179
|
// flush can emit it before the ask prompt. Role-scoped: message_end also
|
|
1077
1180
|
// fires for the user prompt, which must never be mirrored back as turn output.
|
|
1078
1181
|
if ((event.message as { role?: unknown }).role === "assistant") {
|
|
1079
|
-
const turnText = summaryFromMessage(event.message,
|
|
1182
|
+
const turnText = summaryFromMessage(event.message, turnTextMax());
|
|
1080
1183
|
if (turnText) rt.currentTurnText = turnText;
|
|
1081
1184
|
}
|
|
1082
1185
|
for (const img of imageAttachmentsFromMessage(event.message)) {
|
|
@@ -1099,4 +1202,4 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
1099
1202
|
api.on("session_shutdown", async (_event, ctx) => {
|
|
1100
1203
|
await stopSession(sessionId(ctx));
|
|
1101
1204
|
});
|
|
1102
|
-
}
|
|
1205
|
+
}
|
|
@@ -15,6 +15,19 @@ import * as os from "node:os";
|
|
|
15
15
|
import type { SessionCloseTarget, SessionCreateTarget, SessionLifecycleResponse, SessionResumeTarget } from "./index";
|
|
16
16
|
|
|
17
17
|
export type LifecycleCommandVerb = "session_create" | "session_close" | "session_resume";
|
|
18
|
+
function normalizeLifecycleCommandToken(
|
|
19
|
+
token: string,
|
|
20
|
+
ctx: { chatType?: string; botUsername?: string } = {},
|
|
21
|
+
): string | undefined {
|
|
22
|
+
const at = token.indexOf("@");
|
|
23
|
+
const command = at === -1 ? token : token.slice(0, at);
|
|
24
|
+
if (!/^\/session_(create|close|resume|recent)\b/.test(command)) return undefined;
|
|
25
|
+
if (at === -1) return ctx.chatType === undefined || ctx.chatType === "private" ? command : undefined;
|
|
26
|
+
|
|
27
|
+
const botUsername = ctx.botUsername;
|
|
28
|
+
if (!botUsername || token.slice(at + 1).toLowerCase() !== botUsername.toLowerCase()) return undefined;
|
|
29
|
+
return command;
|
|
30
|
+
}
|
|
18
31
|
|
|
19
32
|
/** A parsed, validated lifecycle command (transport identity added by caller). */
|
|
20
33
|
export type ParsedLifecycleCommand =
|
|
@@ -36,10 +49,24 @@ const USAGE = [
|
|
|
36
49
|
"/session_recent [create|resume]",
|
|
37
50
|
].join("\n");
|
|
38
51
|
|
|
39
|
-
/** True when the text begins
|
|
40
|
-
export function
|
|
52
|
+
/** True when the text begins with any /session_* token, regardless of addressability. */
|
|
53
|
+
export function isLifecycleCommandLikeText(text: string | undefined): boolean {
|
|
54
|
+
if (!text) return false;
|
|
55
|
+
const [token] = text.trim().split(/\s+/, 1);
|
|
56
|
+
if (!token) return false;
|
|
57
|
+
const at = token.indexOf("@");
|
|
58
|
+
const command = at === -1 ? token : token.slice(0, at);
|
|
59
|
+
return /^\/session_(create|close|resume|recent)\b/.test(command);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True when the text begins an addressable /session_* command (cheap pre-gate). */
|
|
63
|
+
export function isLifecycleCommandText(
|
|
64
|
+
text: string | undefined,
|
|
65
|
+
ctx: { chatType?: string; botUsername?: string } = {},
|
|
66
|
+
): boolean {
|
|
41
67
|
if (!text) return false;
|
|
42
|
-
|
|
68
|
+
const [rawCommand] = text.trim().split(/\s+/, 1);
|
|
69
|
+
return normalizeLifecycleCommandToken(rawCommand ?? "", ctx) !== undefined;
|
|
43
70
|
}
|
|
44
71
|
|
|
45
72
|
/**
|
|
@@ -50,9 +77,14 @@ export function isLifecycleCommandText(text: string | undefined): boolean {
|
|
|
50
77
|
* The caller MUST have already enforced paired-chat authorization; this function
|
|
51
78
|
* performs grammar + target validation only.
|
|
52
79
|
*/
|
|
53
|
-
export function parseLifecycleCommand(
|
|
54
|
-
|
|
80
|
+
export function parseLifecycleCommand(
|
|
81
|
+
text: string | undefined,
|
|
82
|
+
ctx: { chatType?: string; botUsername?: string } = {},
|
|
83
|
+
): ParsedLifecycleCommand {
|
|
55
84
|
const raw = (text ?? "").trim();
|
|
85
|
+
const [rawCommand, ...args] = raw.split(/\s+/);
|
|
86
|
+
const command = normalizeLifecycleCommandToken(rawCommand ?? "", ctx);
|
|
87
|
+
if (command === undefined) return { kind: "none" };
|
|
56
88
|
|
|
57
89
|
// MVP: reject any initial-prompt separator outright (no prompt handling yet).
|
|
58
90
|
if (/\s--(\s|$)/.test(raw)) {
|
|
@@ -63,8 +95,6 @@ export function parseLifecycleCommand(text: string | undefined): ParsedLifecycle
|
|
|
63
95
|
};
|
|
64
96
|
}
|
|
65
97
|
|
|
66
|
-
const [command, ...args] = raw.split(/\s+/);
|
|
67
|
-
|
|
68
98
|
if (command === "/session_recent") {
|
|
69
99
|
const which = args[0];
|
|
70
100
|
if (which === undefined || which === "create" || which === "resume") {
|
|
@@ -252,7 +252,17 @@ export async function handleLifecycleRequest(
|
|
|
252
252
|
return { status: "error", reason: "rate_limited", message: "create rate limit exceeded" };
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
// 4.
|
|
255
|
+
// 4. Close is intentionally force-only until a graceful close contract exists.
|
|
256
|
+
if (frame.type === "session_close" && frame.force !== true) {
|
|
257
|
+
await deps.audit({ ...baseAudit, event: "rejected", reason: "invalid_target" });
|
|
258
|
+
return {
|
|
259
|
+
status: "error",
|
|
260
|
+
reason: "invalid_target",
|
|
261
|
+
message: "session_close requires force=true; graceful close is not supported",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 5. Preallocate ids + write in_progress (fsynced) BEFORE any spawn.
|
|
256
266
|
const lifecycleRequestId = frame.type === "session_create" ? frame.lifecycleRequestId : deps.newLifecycleRequestId();
|
|
257
267
|
const intendedSessionId =
|
|
258
268
|
frame.type === "session_create" ? frame.intendedSessionId || deps.newSessionId() : deps.newSessionId();
|
|
@@ -344,15 +354,18 @@ export async function handleLifecycleRequest(
|
|
|
344
354
|
await deps.audit({ ...baseAudit, event: "success", sessionId: resumed.sessionId });
|
|
345
355
|
return { status: "ok", entry, mode: resumed.mode };
|
|
346
356
|
} catch (err) {
|
|
347
|
-
// A side effect may have occurred; do not
|
|
348
|
-
// uncertain so a retry reconciles instead of duplicating.
|
|
357
|
+
// A side effect may have occurred; do not repeat it automatically. Mark
|
|
358
|
+
// terminal-uncertain so a retry reconciles instead of duplicating work.
|
|
359
|
+
let reason: LifecycleErrorReason = "terminal_uncertain";
|
|
360
|
+
if (frame.type === "session_create") reason = "spawn_failed";
|
|
361
|
+
if (frame.type === "session_close") reason = "close_refused";
|
|
349
362
|
Object.assign(entry, {
|
|
350
363
|
state: "terminal_uncertain",
|
|
351
364
|
updatedAt: deps.now(),
|
|
352
|
-
reason
|
|
365
|
+
reason,
|
|
353
366
|
});
|
|
354
367
|
await deps.store.write(doc);
|
|
355
|
-
await deps.audit({ ...baseAudit, event: "terminal_uncertain", reason
|
|
356
|
-
return { status: "error", reason: "terminal_uncertain", message:
|
|
368
|
+
await deps.audit({ ...baseAudit, event: "terminal_uncertain", reason });
|
|
369
|
+
return { status: "error", reason: "terminal_uncertain", message: `${frame.type} effect failed: ${String(err)}` };
|
|
357
370
|
}
|
|
358
371
|
}
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
DaemonStatus,
|
|
21
21
|
} from "../daemon/control-types";
|
|
22
22
|
import { resolveSkcRuntimeSpawnInfo } from "../daemon/runtime";
|
|
23
|
-
import { getNotificationConfig,
|
|
23
|
+
import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
|
|
24
24
|
import {
|
|
25
25
|
daemonPaths,
|
|
26
26
|
isFreshLiveOwner,
|
|
@@ -159,7 +159,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
159
159
|
async status(): Promise<DaemonStatus> {
|
|
160
160
|
const runtime = this.runtimeInfo();
|
|
161
161
|
const cfg = getNotificationConfig(this.settings);
|
|
162
|
-
const configured =
|
|
162
|
+
const configured = isTelegramConfigured(cfg);
|
|
163
163
|
if (!configured) {
|
|
164
164
|
return { kind: this.kind, configured: false, health: "not_configured", runtime };
|
|
165
165
|
}
|
|
@@ -269,6 +269,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
269
269
|
{ settings: this.settings, roots, tokenFingerprint: fp, chatId },
|
|
270
270
|
this.spawnDeps(),
|
|
271
271
|
);
|
|
272
|
+
warnings.push(...spawned.warnings);
|
|
272
273
|
const after = await this.status();
|
|
273
274
|
return this.result(
|
|
274
275
|
action,
|
|
@@ -351,6 +352,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
351
352
|
{ settings: this.settings, roots, tokenFingerprint: fp, chatId },
|
|
352
353
|
this.spawnDeps(),
|
|
353
354
|
);
|
|
355
|
+
warnings.push(...spawned.warnings);
|
|
354
356
|
const after = await this.status();
|
|
355
357
|
if (spawned.result === "attached") {
|
|
356
358
|
// A live owner already exists; attaching to it is a valid running end-state.
|
|
@@ -360,7 +362,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
|
|
|
360
362
|
}
|
|
361
363
|
return this.result(
|
|
362
364
|
action,
|
|
363
|
-
spawned.result !== "disabled",
|
|
365
|
+
spawned.result !== "disabled" && spawned.result !== "blocked",
|
|
364
366
|
`reloaded telegram daemon (${spawned.result})`,
|
|
365
367
|
before,
|
|
366
368
|
after,
|