@sayknow-cli/coding-agent 0.2.7 → 0.3.1

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.
Files changed (216) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/notify-cli.d.ts +25 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +136 -3
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  18. package/dist/types/extensibility/shared-events.d.ts +1 -0
  19. package/dist/types/i18n/messages/en.d.ts +1 -0
  20. package/dist/types/lsp/types.d.ts +2 -0
  21. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  22. package/dist/types/modes/components/settings-defs.d.ts +1 -0
  23. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  24. package/dist/types/modes/interactive-mode.d.ts +1 -1
  25. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  26. package/dist/types/modes/theme/theme.d.ts +1 -1
  27. package/dist/types/modes/types.d.ts +7 -1
  28. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  29. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  30. package/dist/types/notifications/config-commands.d.ts +26 -0
  31. package/dist/types/notifications/config.d.ts +69 -0
  32. package/dist/types/notifications/engine.d.ts +59 -0
  33. package/dist/types/notifications/helpers.d.ts +55 -0
  34. package/dist/types/notifications/html-format.d.ts +62 -0
  35. package/dist/types/notifications/index.d.ts +28 -0
  36. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  37. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  38. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  39. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  40. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  41. package/dist/types/notifications/telegram-daemon.d.ts +295 -0
  42. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  43. package/dist/types/notifications/threaded-inbound.d.ts +77 -0
  44. package/dist/types/notifications/threaded-render.d.ts +71 -0
  45. package/dist/types/notifications/topic-registry.d.ts +67 -0
  46. package/dist/types/rlm/index.d.ts +12 -0
  47. package/dist/types/session/agent-session.d.ts +41 -2
  48. package/dist/types/session/auth-storage.d.ts +1 -1
  49. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  50. package/dist/types/setup/credential-import.d.ts +3 -0
  51. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  52. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  53. package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
  54. package/dist/types/skc-runtime/state-writer.d.ts +2 -0
  55. package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
  56. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  57. package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
  58. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
  59. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  60. package/dist/types/tools/fetch.d.ts +23 -0
  61. package/dist/types/tools/index.d.ts +19 -0
  62. package/dist/types/tools/subagent.d.ts +3 -0
  63. package/dist/types/tools/telegram-send.d.ts +32 -0
  64. package/dist/types/web/insane/bridge.d.ts +103 -0
  65. package/dist/types/web/insane/url-guard.d.ts +22 -0
  66. package/dist/types/web/search/provider.d.ts +18 -1
  67. package/dist/types/web/search/providers/insane.d.ts +53 -0
  68. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  69. package/dist/types/web/search/types.d.ts +12 -4
  70. package/package.json +10 -8
  71. package/scripts/build-binary.ts +3 -0
  72. package/scripts/verify-insane-vendor.ts +132 -0
  73. package/src/async/job-manager.ts +5 -1
  74. package/src/cli/args.ts +1 -1
  75. package/src/cli/daemon-cli.ts +122 -0
  76. package/src/cli/fast-help.ts +1 -1
  77. package/src/cli/notify-cli.ts +421 -0
  78. package/src/cli/setup-cli.ts +173 -84
  79. package/src/cli.ts +3 -3
  80. package/src/commands/daemon.ts +47 -0
  81. package/src/commands/notify.ts +61 -0
  82. package/src/commands/setup.ts +11 -1
  83. package/src/commands/team.ts +1 -1
  84. package/src/config/model-profile-activation.ts +74 -5
  85. package/src/config/model-profiles.ts +7 -4
  86. package/src/config/model-registry.ts +6 -3
  87. package/src/config/models-config-schema.ts +1 -1
  88. package/src/config/settings-schema.ts +143 -0
  89. package/src/coordinator/contract.ts +3 -0
  90. package/src/coordinator-mcp/server.ts +270 -1
  91. package/src/daemon/builtin.ts +46 -0
  92. package/src/daemon/control-types.ts +65 -0
  93. package/src/daemon/runtime.ts +51 -0
  94. package/src/defaults/skc/rules/ponytail.md +68 -0
  95. package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
  96. package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
  97. package/src/edit/modes/replace.ts +1 -1
  98. package/src/extensibility/extensions/runner.ts +4 -0
  99. package/src/extensibility/extensions/types.ts +8 -0
  100. package/src/extensibility/shared-events.ts +1 -0
  101. package/src/goals/tools/goal-tool.ts +11 -2
  102. package/src/hashline/hash.ts +1 -1
  103. package/src/i18n/messages/de.ts +1 -0
  104. package/src/i18n/messages/en.ts +1 -0
  105. package/src/i18n/messages/es.ts +1 -0
  106. package/src/i18n/messages/fr.ts +1 -0
  107. package/src/i18n/messages/ja.ts +1 -0
  108. package/src/i18n/messages/ko.ts +1 -0
  109. package/src/i18n/messages/zh.ts +1 -0
  110. package/src/internal-urls/docs-index.generated.ts +12 -10
  111. package/src/lsp/config.ts +16 -3
  112. package/src/lsp/defaults.json +7 -0
  113. package/src/lsp/types.ts +2 -0
  114. package/src/main.ts +30 -0
  115. package/src/modes/acp/acp-event-mapper.ts +1 -0
  116. package/src/modes/components/hook-editor.ts +7 -2
  117. package/src/modes/components/oauth-selector.ts +19 -0
  118. package/src/modes/components/settings-defs.ts +2 -1
  119. package/src/modes/components/settings-selector.ts +7 -2
  120. package/src/modes/controllers/event-controller.ts +35 -0
  121. package/src/modes/controllers/selector-controller.ts +80 -17
  122. package/src/modes/interactive-mode.ts +52 -4
  123. package/src/modes/runtime-init.ts +1 -0
  124. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  125. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  126. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  127. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  128. package/src/modes/theme/theme.ts +4 -0
  129. package/src/modes/types.ts +7 -1
  130. package/src/modes/utils/context-usage.ts +2 -2
  131. package/src/modes/utils/ui-helpers.ts +23 -0
  132. package/src/notifications/attachment-registry.ts +23 -0
  133. package/src/notifications/chat-adapters.ts +147 -0
  134. package/src/notifications/config-commands.ts +50 -0
  135. package/src/notifications/config.ts +128 -0
  136. package/src/notifications/engine.ts +100 -0
  137. package/src/notifications/helpers.ts +135 -0
  138. package/src/notifications/html-format.ts +389 -0
  139. package/src/notifications/index.ts +842 -0
  140. package/src/notifications/managed-daemon.ts +163 -0
  141. package/src/notifications/rate-limit-pool.ts +179 -0
  142. package/src/notifications/telegram-cli.ts +194 -0
  143. package/src/notifications/telegram-daemon-cli.ts +74 -0
  144. package/src/notifications/telegram-daemon-control.ts +370 -0
  145. package/src/notifications/telegram-daemon.ts +1591 -0
  146. package/src/notifications/telegram-reference.ts +335 -0
  147. package/src/notifications/threaded-inbound.ts +136 -0
  148. package/src/notifications/threaded-render.ts +173 -0
  149. package/src/notifications/topic-registry.ts +133 -0
  150. package/src/rlm/index.ts +19 -0
  151. package/src/sdk.ts +16 -0
  152. package/src/session/agent-session.ts +195 -54
  153. package/src/session/auth-storage.ts +3 -0
  154. package/src/session/session-dump-format.ts +43 -2
  155. package/src/session/session-manager.ts +39 -5
  156. package/src/setup/credential-auto-import.ts +258 -0
  157. package/src/setup/credential-import.ts +17 -0
  158. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  159. package/src/setup/host-plugin-setup.ts +142 -0
  160. package/src/skc-runtime/deep-interview-recorder.ts +2 -2
  161. package/src/skc-runtime/launch-tmux.ts +27 -5
  162. package/src/skc-runtime/ledger-event-renderer.ts +1 -0
  163. package/src/skc-runtime/ralplan-runtime.ts +2 -2
  164. package/src/skc-runtime/state-runtime.ts +18 -10
  165. package/src/skc-runtime/state-writer.ts +8 -8
  166. package/src/skc-runtime/tmux-common.ts +8 -0
  167. package/src/skc-runtime/tmux-sessions.ts +8 -1
  168. package/src/skc-runtime/ultragoal-guard.ts +57 -2
  169. package/src/skc-runtime/ultragoal-runtime.ts +105 -19
  170. package/src/skc-runtime/workflow-manifest.generated.json +56 -2
  171. package/src/skc-runtime/workflow-manifest.ts +18 -3
  172. package/src/slash-commands/builtin-registry.ts +4 -1
  173. package/src/task/executor.ts +5 -1
  174. package/src/tools/ask-answer-registry.ts +25 -0
  175. package/src/tools/ask.ts +77 -6
  176. package/src/tools/fetch.ts +78 -1
  177. package/src/tools/image-gen.ts +5 -8
  178. package/src/tools/index.ts +22 -0
  179. package/src/tools/inspect-image.ts +16 -11
  180. package/src/tools/subagent-render.ts +7 -0
  181. package/src/tools/subagent.ts +38 -7
  182. package/src/tools/telegram-send.ts +137 -0
  183. package/src/web/insane/bridge.ts +350 -0
  184. package/src/web/insane/url-guard.ts +155 -0
  185. package/src/web/search/provider.ts +77 -18
  186. package/src/web/search/providers/anthropic.ts +70 -3
  187. package/src/web/search/providers/codex.ts +1 -119
  188. package/src/web/search/providers/gemini.ts +99 -0
  189. package/src/web/search/providers/insane.ts +551 -0
  190. package/src/web/search/providers/openai-compatible.ts +66 -32
  191. package/src/web/search/providers/text-citations.ts +111 -0
  192. package/src/web/search/types.ts +13 -2
  193. package/vendor/insane-search/LICENSE +21 -0
  194. package/vendor/insane-search/MANIFEST.json +24 -0
  195. package/vendor/insane-search/engine/__init__.py +23 -0
  196. package/vendor/insane-search/engine/__main__.py +128 -0
  197. package/vendor/insane-search/engine/bias_check.py +183 -0
  198. package/vendor/insane-search/engine/executor.py +254 -0
  199. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  200. package/vendor/insane-search/engine/learning.py +175 -0
  201. package/vendor/insane-search/engine/phase0.py +214 -0
  202. package/vendor/insane-search/engine/safety.py +91 -0
  203. package/vendor/insane-search/engine/templates/package.json +11 -0
  204. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  205. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  206. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  207. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  208. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  209. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  210. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  211. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  212. package/vendor/insane-search/engine/transport.py +211 -0
  213. package/vendor/insane-search/engine/url_transforms.py +98 -0
  214. package/vendor/insane-search/engine/validators.py +331 -0
  215. package/vendor/insane-search/engine/waf_detector.py +214 -0
  216. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Per-session forum-topic registry for the threaded session surface.
3
+ *
4
+ * Each SKC session owns exactly one Telegram forum topic in the paired private
5
+ * DM. The topic is created once (via `createForumTopic`) and REUSED on resume,
6
+ * keyed by session id, so a resumed session streams back into its existing
7
+ * thread/history. The registry also tracks whether the one-time identity header
8
+ * has already been pinned, so it is sent exactly once per topic even across
9
+ * reconnects.
10
+ *
11
+ * State is a plain serialisable map persisted beside the daemon state files;
12
+ * topic creation is injected so this module is pure and unit-testable without a
13
+ * live Bot API.
14
+ */
15
+
16
+ /** Persisted record for one session's topic. */
17
+ export interface TopicRecord {
18
+ /** Telegram forum topic id (message_thread_id). */
19
+ topicId: string;
20
+ /** Whether the one-time identity header has been sent/pinned. */
21
+ identitySent: boolean;
22
+ /** Creation timestamp (ms epoch). */
23
+ createdAt: number;
24
+ /** Last applied topic title (for rename detection). */
25
+ name?: string;
26
+ }
27
+
28
+ /** Serialisable shape persisted to disk. */
29
+ export interface TopicRegistryState {
30
+ /** sessionId -> record. */
31
+ topics: Record<string, TopicRecord>;
32
+ }
33
+
34
+ export function emptyTopicRegistryState(): TopicRegistryState {
35
+ return { topics: {} };
36
+ }
37
+
38
+ /**
39
+ * In-memory registry over a serialisable state. Topic creation is injected via
40
+ * `getOrCreateTopic`'s `create` callback (the daemon supplies a real
41
+ * `createForumTopic` call); reuse-on-resume is automatic when a record exists.
42
+ */
43
+ export class TopicRegistry {
44
+ private readonly topics: Map<string, TopicRecord>;
45
+ /** Maps topicId -> sessionId for fast inbound routing. */
46
+ private readonly byTopic = new Map<string, string>();
47
+ /** In-flight create promises, keyed by session, to dedupe concurrent creates. */
48
+ private readonly inflight = new Map<string, Promise<TopicRecord>>();
49
+
50
+ constructor(state: TopicRegistryState = emptyTopicRegistryState()) {
51
+ this.topics = new Map(Object.entries(state.topics ?? {}));
52
+ for (const [sessionId, record] of this.topics) this.byTopic.set(record.topicId, sessionId);
53
+ }
54
+
55
+ /** Merge a serialized state into this registry, preserving all persisted fields. */
56
+ load(state: TopicRegistryState): void {
57
+ for (const [sessionId, record] of Object.entries(state.topics ?? {})) {
58
+ this.topics.set(sessionId, record);
59
+ this.byTopic.set(record.topicId, sessionId);
60
+ }
61
+ }
62
+
63
+ /** Resolve the owning session for a topic id (for fail-closed inbound routing). */
64
+ sessionForTopic(topicId: string): string | undefined {
65
+ return this.byTopic.get(topicId);
66
+ }
67
+
68
+ /** The existing topic record for a session, if any. */
69
+ get(sessionId: string): TopicRecord | undefined {
70
+ return this.topics.get(sessionId);
71
+ }
72
+
73
+ /**
74
+ * Return the existing topic for `sessionId`, or create one via `create`
75
+ * (called only on first use). Reuse-on-resume: an existing record is
76
+ * returned without invoking `create`.
77
+ */
78
+ async getOrCreateTopic(
79
+ sessionId: string,
80
+ create: () => Promise<string>,
81
+ now: () => number = Date.now,
82
+ ): Promise<TopicRecord> {
83
+ const existing = this.topics.get(sessionId);
84
+ if (existing) return existing;
85
+ // Concurrency guard: many session frames (identity/idle/turn/ask) can race
86
+ // to first-use the same session. Without this, each call passes the
87
+ // `existing` check before `create()` resolves and creates a DUPLICATE
88
+ // forum topic. Share a single in-flight create per session id.
89
+ const pending = this.inflight.get(sessionId);
90
+ if (pending) return pending;
91
+ const promise = (async () => {
92
+ const topicId = await create();
93
+ const record: TopicRecord = { topicId, identitySent: false, createdAt: now() };
94
+ this.topics.set(sessionId, record);
95
+ this.byTopic.set(topicId, sessionId);
96
+ return record;
97
+ })();
98
+ this.inflight.set(sessionId, promise);
99
+ try {
100
+ return await promise;
101
+ } finally {
102
+ this.inflight.delete(sessionId);
103
+ }
104
+ }
105
+
106
+ /** Mark the identity header as sent for a session. Idempotent. */
107
+ markIdentitySent(sessionId: string): void {
108
+ const record = this.topics.get(sessionId);
109
+ if (record) record.identitySent = true;
110
+ }
111
+
112
+ /** Whether the identity header still needs sending for this session. */
113
+ needsIdentity(sessionId: string): boolean {
114
+ const record = this.topics.get(sessionId);
115
+ return record ? !record.identitySent : true;
116
+ }
117
+
118
+ /**
119
+ * Record the topic's applied title. Returns `true` when it changed (so the
120
+ * caller should `editForumTopic`), `false` when already current or unknown.
121
+ */
122
+ applyName(sessionId: string, name: string): boolean {
123
+ const record = this.topics.get(sessionId);
124
+ if (!record || record.name === name) return false;
125
+ record.name = name;
126
+ return true;
127
+ }
128
+
129
+ /** Serialise for atomic persistence beside the daemon state. */
130
+ serialize(): TopicRegistryState {
131
+ return { topics: Object.fromEntries(this.topics) };
132
+ }
133
+ }
package/src/rlm/index.ts CHANGED
@@ -240,8 +240,27 @@ async function writeRlmMetadata(input: {
240
240
  }
241
241
  }
242
242
 
243
+ /**
244
+ * RLM artifacts are scoped under a SKC session directory and resolving their
245
+ * paths is a *write* (it must pick a concrete session). When `skc rlm` runs
246
+ * standalone — no parent agent, no `SKC_SESSION_ID` in the environment — there is
247
+ * no session to resolve and `resolveSkcSessionForWrite` throws
248
+ * `missing_for_write`. Establish a dedicated SKC session id in that case and pin
249
+ * it into the environment so artifact-path resolution, the per-session activity
250
+ * marker, and the child agent's workflow state all share one writable session.
251
+ *
252
+ * Returns the resolved (existing or freshly generated) SKC session id.
253
+ */
254
+ export function ensureRlmSkcSessionId(): string {
255
+ const existing = resolveSessionIdFromSources({ envSessionId: process.env.SKC_SESSION_ID })?.skcSessionId;
256
+ if (existing) return existing;
257
+ const generated = `rlm-${generateRlmSessionId()}`;
258
+ process.env.SKC_SESSION_ID = generated;
259
+ return generated;
260
+ }
243
261
  export async function runRlmCommand(argv: string[]): Promise<void> {
244
262
  const cwd = getProjectDir();
263
+ ensureRlmSkcSessionId();
245
264
  const { dataPath, resumeSessionId, minSuccessfulRuns, rest } = extractRlmFlags(argv);
246
265
  const dataContext = await loadRlmDataContext(cwd, dataPath);
247
266
 
package/src/sdk.ts CHANGED
@@ -81,6 +81,12 @@ import type { HindsightSessionState } from "./hindsight/state";
81
81
  import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
82
82
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
83
83
  import { resolveMemoryBackend } from "./memory-backend";
84
+ import { createNotificationsExtension } from "./notifications";
85
+ import {
86
+ getNotificationConfig,
87
+ type NotificationConfig,
88
+ shouldRegisterNotificationsExtension,
89
+ } from "./notifications/config";
84
90
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
85
91
  import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
86
92
  import { MCPManager } from "./runtime-mcp";
@@ -1240,6 +1246,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1240
1246
  getPlanModeState: () => session?.getPlanModeState(),
1241
1247
  getGoalModeState: () => session?.getGoalModeState(),
1242
1248
  getWorkflowGateEmitter: () => session?.getWorkflowGateEmitter(),
1249
+ getAskAnswerSource: () => session?.getAskAnswerSource(),
1243
1250
  getGoalRuntime: () => session?.goalRuntime,
1244
1251
  getClientBridge: () => session?.clientBridge,
1245
1252
  getCompactContext: () => session.formatCompactContext(),
@@ -1388,6 +1395,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1388
1395
  if (customTools.length > 0) {
1389
1396
  inlineExtensions.push(createCustomToolsExtension(customTools));
1390
1397
  }
1398
+ let notificationCfg: NotificationConfig | undefined;
1399
+ try {
1400
+ notificationCfg = getNotificationConfig(Settings.instance);
1401
+ } catch {
1402
+ notificationCfg = undefined;
1403
+ }
1404
+ if (shouldRegisterNotificationsExtension({ env: process.env, cfg: notificationCfg })) {
1405
+ inlineExtensions.push(createNotificationsExtension);
1406
+ }
1391
1407
 
1392
1408
  // Extension/module discovery is quarantined; retain only the private
1393
1409
  // runtime needed for bundled product extensions, explicitly supplied SDK
@@ -238,8 +238,9 @@ import {
238
238
  type DiscoverableTool,
239
239
  type DiscoverableToolSearchIndex,
240
240
  } from "../tool-discovery/tool-index";
241
- import type { ToolSession } from "../tools";
241
+ import type { AskAnswerSource, ToolSession } from "../tools";
242
242
  import { AskTool } from "../tools/ask";
243
+ import { getAskAnswerSource as getAskAnswerSourceFromRegistry } from "../tools/ask-answer-registry";
243
244
  import { assertEditableFile } from "../tools/auto-generated-guard";
244
245
  import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
245
246
  import type { CheckpointState } from "../tools/checkpoint";
@@ -285,6 +286,8 @@ import { ToolChoiceQueue } from "./tool-choice-queue";
285
286
  import { YieldQueue } from "./yield-queue";
286
287
 
287
288
  /** Session-specific events that extend the core AgentEvent */
289
+ export type AutoCompactionContinuationSkipReason = "auto_continue_disabled_non_resumable_tail";
290
+
288
291
  export type AgentSessionEvent =
289
292
  | AgentEvent
290
293
  | { type: "auto_compaction_start"; reason: "threshold" | "overflow" | "idle"; action: "context-full" | "handoff" }
@@ -297,6 +300,7 @@ export type AgentSessionEvent =
297
300
  errorMessage?: string;
298
301
  /** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
299
302
  skipped?: boolean;
303
+ continuationSkipReason?: AutoCompactionContinuationSkipReason;
300
304
  }
301
305
  | {
302
306
  type: "auto_retry_start";
@@ -313,6 +317,7 @@ export type AgentSessionEvent =
313
317
  | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
314
318
  | { type: "todo_auto_clear" }
315
319
  | { type: "irc_message"; message: CustomMessage }
320
+ | { type: "subagent_steer_message"; message: CustomMessage }
316
321
  | { type: "notice"; level: "info" | "warning" | "error"; message: string; source?: string }
317
322
  | { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
318
323
  | { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
@@ -1867,7 +1872,6 @@ export class AgentSession {
1867
1872
  this.#resolveTtsrResume();
1868
1873
  return;
1869
1874
  }
1870
- this.#ttsrAbortPending = false;
1871
1875
  this.#perToolTtsrInjections.clear();
1872
1876
  const ttsrSettings = this.#ttsrManager?.getSettings();
1873
1877
  if (ttsrSettings?.contextMode === "discard" && targetAssistantIndex !== -1) {
@@ -1896,11 +1900,22 @@ export class AgentSession {
1896
1900
  );
1897
1901
  this.#markTtsrInjected(details.rules);
1898
1902
  }
1899
- try {
1900
- await this.agent.continue();
1901
- } catch {
1902
- this.#resolveTtsrResume();
1903
- }
1903
+ await this.#scheduleAgentContinue({
1904
+ delayMs: 0,
1905
+ generation,
1906
+ shouldContinue: () => {
1907
+ this.#ttsrAbortPending = false;
1908
+ return true;
1909
+ },
1910
+ onSkip: () => {
1911
+ this.#ttsrAbortPending = false;
1912
+ this.#resolveTtsrResume();
1913
+ },
1914
+ onError: () => {
1915
+ this.#ttsrAbortPending = false;
1916
+ this.#resolveTtsrResume();
1917
+ },
1918
+ });
1904
1919
  },
1905
1920
  { delayMs: 50 },
1906
1921
  );
@@ -2212,7 +2227,7 @@ export class AgentSession {
2212
2227
  #schedulePostPromptTask(
2213
2228
  task: (signal: AbortSignal) => Promise<void>,
2214
2229
  options?: { delayMs?: number; generation?: number; onSkip?: () => void },
2215
- ): void {
2230
+ ): Promise<void> {
2216
2231
  const delayMs = options?.delayMs ?? 0;
2217
2232
  const signal = this.#postPromptTasksAbortController.signal;
2218
2233
  const scheduled = (async () => {
@@ -2234,18 +2249,20 @@ export class AgentSession {
2234
2249
  await task(signal);
2235
2250
  })();
2236
2251
  this.#trackPostPromptTask(scheduled);
2252
+ return scheduled;
2237
2253
  }
2238
2254
 
2239
2255
  #scheduleAgentContinue(options?: {
2240
2256
  delayMs?: number;
2241
2257
  generation?: number;
2258
+ skipCompactionCheck?: boolean;
2242
2259
  shouldContinue?: () => boolean;
2243
2260
  onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained") => void;
2244
2261
  onError?: (error: unknown) => void;
2245
- }): void {
2262
+ }): Promise<void> {
2246
2263
  const scheduledGeneration = options?.generation;
2247
2264
  const signal = this.#postPromptTasksAbortController.signal;
2248
- this.#schedulePostPromptTask(
2265
+ return this.#schedulePostPromptTask(
2249
2266
  async () => {
2250
2267
  if (signal.aborted) {
2251
2268
  options?.onSkip?.("aborted_signal");
@@ -2261,6 +2278,9 @@ export class AgentSession {
2261
2278
  }
2262
2279
  try {
2263
2280
  await this.#maybeRestoreRetryFallbackPrimary();
2281
+ if (!options?.skipCompactionCheck) {
2282
+ await this.#checkEstimatedContextBeforePrompt();
2283
+ }
2264
2284
  await this.agent.continue();
2265
2285
  } catch (error) {
2266
2286
  logger.warn("agent.continue failed after scheduling", {
@@ -2305,6 +2325,34 @@ export class AgentSession {
2305
2325
  }
2306
2326
  }
2307
2327
 
2328
+ #detectOverflowRetryContinuationSkip(): AutoCompactionContinuationSkipReason | undefined {
2329
+ this.#stripOverflowFailedTurnForRetry();
2330
+ if (this.#isResumableAgentTail()) return undefined;
2331
+ const compactionSettings = this.settings.getGroup("compaction");
2332
+ return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
2333
+ }
2334
+
2335
+ #scheduleOverflowRetryContinuation(generation: number): void {
2336
+ this.#stripOverflowFailedTurnForRetry();
2337
+ if (this.#isResumableAgentTail()) {
2338
+ this.#scheduleAgentContinue({
2339
+ delayMs: 100,
2340
+ generation,
2341
+ onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
2342
+ onError: error => this.#logCompactionContinuationError("overflow_retry", error),
2343
+ });
2344
+ return;
2345
+ }
2346
+
2347
+ const compactionSettings = this.settings.getGroup("compaction");
2348
+ if (compactionSettings.autoContinue !== false) {
2349
+ this.#scheduleAutoContinuePrompt(generation);
2350
+ return;
2351
+ }
2352
+
2353
+ this.#logCompactionContinuationSkipped("overflow_retry", "auto_continue_disabled_non_resumable_tail");
2354
+ }
2355
+
2308
2356
  #scheduleAutoContinuePrompt(generation: number): void {
2309
2357
  const continuePrompt = async () => {
2310
2358
  await this.#promptWithMessage(
@@ -3050,6 +3098,7 @@ export class AgentSession {
3050
3098
  willRetry: event.willRetry,
3051
3099
  errorMessage: event.errorMessage,
3052
3100
  skipped: event.skipped,
3101
+ continuationSkipReason: event.continuationSkipReason,
3053
3102
  });
3054
3103
  } else if (event.type === "auto_retry_start") {
3055
3104
  await this.#extensionRunner.emit({
@@ -4385,6 +4434,10 @@ export class AgentSession {
4385
4434
  return this.#workflowGateEmitter;
4386
4435
  }
4387
4436
 
4437
+ getAskAnswerSource(): AskAnswerSource | undefined {
4438
+ return getAskAnswerSourceFromRegistry(this.sessionId);
4439
+ }
4440
+
4388
4441
  setWorkflowGateEmitter(emitter: WorkflowGateEmitter | undefined): void {
4389
4442
  this.#workflowGateEmitter = emitter;
4390
4443
  if (emitter) {
@@ -5453,6 +5506,16 @@ export class AgentSession {
5453
5506
  return;
5454
5507
  }
5455
5508
 
5509
+ // No explicit delivery mode: only a live stream makes prompt() throw
5510
+ // AgentBusyError, so queue the message as steering while streaming.
5511
+ // Compaction is intentionally NOT diverted here: prompt() handles an
5512
+ // in-flight compaction internally, and #queueSteer would otherwise park
5513
+ // the message in the steering queue with no turn to consume it.
5514
+ if (this.isStreaming) {
5515
+ await this.#queueSteer(text, images);
5516
+ return;
5517
+ }
5518
+
5456
5519
  // Use prompt() with expandPromptTemplates: false to skip command handling and template expansion
5457
5520
  await this.prompt(text, {
5458
5521
  expandPromptTemplates: false,
@@ -5856,12 +5919,46 @@ export class AgentSession {
5856
5919
  return this.#activeModelProfile;
5857
5920
  }
5858
5921
 
5922
+ /**
5923
+ * The model selector ("provider/id") that resume restores as the session
5924
+ * default — the latest session-log `model_change` with role="default".
5925
+ * Model-profile activation snapshots this before mutating the session so a
5926
+ * failed-activation rollback can restore the pre-activation resume default
5927
+ * instead of promoting a transient runtime model to the resume default.
5928
+ */
5929
+ getSessionDefaultModelSelector(): string | undefined {
5930
+ return this.sessionManager.buildSessionContext().models.default;
5931
+ }
5932
+
5933
+ /**
5934
+ * Re-assert the session resume default ("provider/id") in the session log
5935
+ * WITHOUT touching the live runtime model. Appends a `model_change` with
5936
+ * role="default"; never writes to global settings (apply-for-this-session
5937
+ * semantics). Used by model-profile activation rollback to neutralize the
5938
+ * profile main model the failed activation already recorded as the default.
5939
+ */
5940
+ recordResumeDefaultModel(selector: string): void {
5941
+ this.sessionManager.appendModelChange(selector, "default");
5942
+ }
5943
+
5859
5944
  /**
5860
5945
  * Set model temporarily (for this session only).
5861
5946
  * Validates API key, saves to session log but NOT to settings.
5947
+ *
5948
+ * The change is recorded in the session log as `role: "temporary"` by
5949
+ * default, which means it is NOT restored as the session default on resume —
5950
+ * transient retry/fallback/context-promotion/plan switches must not clobber
5951
+ * the user's explicit pick (issue #849). Model-profile activation passes
5952
+ * `persistAsSessionDefault: true` so the profile's main model becomes the
5953
+ * session default and survives resume, while still not being written to
5954
+ * global settings (new sessions keep the global default).
5862
5955
  * @throws Error if no API key available for the model
5863
5956
  */
5864
- async setModelTemporary(model: Model, thinkingLevel?: ThinkingLevel): Promise<void> {
5957
+ async setModelTemporary(
5958
+ model: Model,
5959
+ thinkingLevel?: ThinkingLevel,
5960
+ options?: { persistAsSessionDefault?: boolean },
5961
+ ): Promise<void> {
5865
5962
  const previousEditMode = this.#resolveActiveEditMode();
5866
5963
  const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
5867
5964
  if (!apiKey) {
@@ -5870,7 +5967,10 @@ export class AgentSession {
5870
5967
 
5871
5968
  this.#clearActiveRetryFallback();
5872
5969
  this.#setModelWithProviderSessionReset(model);
5873
- this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
5970
+ this.sessionManager.appendModelChange(
5971
+ `${model.provider}/${model.id}`,
5972
+ options?.persistAsSessionDefault ? "default" : "temporary",
5973
+ );
5874
5974
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
5875
5975
 
5876
5976
  // Apply explicit thinking level if given; otherwise prefer the model's
@@ -6669,6 +6769,8 @@ export class AgentSession {
6669
6769
  const compactionSettings = this.settings.getGroup("compaction");
6670
6770
  if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
6671
6771
  await this.#runAutoCompaction("overflow", true);
6772
+ } else {
6773
+ this.#scheduleOverflowRetryContinuation(generation);
6672
6774
  }
6673
6775
  return;
6674
6776
  }
@@ -6679,17 +6781,19 @@ export class AgentSession {
6679
6781
  // Skip if this was an error (non-overflow errors don't have usage data)
6680
6782
  if (assistantMessage.stopReason === "error") return;
6681
6783
  let contextTokens = calculateContextTokens(assistantMessage.usage);
6682
- const maxOutputTokens = this.model?.maxTokens ?? 0;
6784
+ // Model maxTokens is a capability ceiling, not a per-turn reservation.
6785
+ // Auto maintenance should track actual context fullness.
6786
+ const autoCompactionOutputReserveTokens = 0;
6683
6787
  // Cache-epoch invariant: pruning rewrites already-sent toolResult history,
6684
6788
  // which breaks the provider prompt-cache prefix mid-epoch. Only prune at a
6685
6789
  // sanctioned maintenance boundary, i.e. when the un-pruned context already
6686
6790
  // crosses the compaction threshold. Pruning may then avert full compaction.
6687
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
6791
+ if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) return;
6688
6792
  const pruneResult = await this.#pruneToolOutputs();
6689
6793
  if (pruneResult) {
6690
6794
  contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
6691
6795
  }
6692
- if (shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) {
6796
+ if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
6693
6797
  // Try promotion first — if a larger model is available, switch instead of compacting
6694
6798
  const promoted = await this.#tryContextPromotion(assistantMessage);
6695
6799
  if (!promoted) {
@@ -6767,14 +6871,16 @@ export class AgentSession {
6767
6871
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
6768
6872
 
6769
6873
  let contextTokens = this.#estimateContextTokensForCompaction(pendingMessages).tokens;
6770
- const maxOutputTokens = model.maxTokens ?? 0;
6771
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
6874
+ // Model maxTokens is a capability ceiling, not a per-turn reservation.
6875
+ // Auto maintenance should track actual context fullness.
6876
+ const autoCompactionOutputReserveTokens = 0;
6877
+ if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) return;
6772
6878
 
6773
6879
  const pruneResult = await this.#pruneToolOutputs();
6774
6880
  if (pruneResult) {
6775
6881
  contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
6776
6882
  }
6777
- if (shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) {
6883
+ if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
6778
6884
  await this.#runAutoCompaction("threshold", false, false, {
6779
6885
  continueAfterMaintenance: false,
6780
6886
  deferHandoffMaintenance: false,
@@ -7677,32 +7783,18 @@ export class AgentSession {
7677
7783
 
7678
7784
  const preparation = prepareCompaction(pathEntries, compactionSettings);
7679
7785
  if (!preparation) {
7786
+ const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
7680
7787
  await this.#emitSessionEvent({
7681
7788
  type: "auto_compaction_end",
7682
7789
  action,
7683
7790
  result: undefined,
7684
7791
  aborted: false,
7685
- willRetry: false,
7792
+ willRetry: willRetry && !continuationSkipReason,
7686
7793
  skipped: true,
7794
+ continuationSkipReason,
7687
7795
  });
7688
7796
  if (willRetry) {
7689
- this.#stripOverflowFailedTurnForRetry();
7690
- if (this.#isResumableAgentTail()) {
7691
- this.#scheduleAgentContinue({
7692
- delayMs: 100,
7693
- generation,
7694
- onSkip: skipReason => this.#logCompactionContinuationSkipped("overflow_retry", skipReason),
7695
- onError: error => this.#logCompactionContinuationError("overflow_retry", error),
7696
- });
7697
- } else {
7698
- const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
7699
- logger.warn("Auto-compaction continuation skipped", {
7700
- source: "overflow_retry",
7701
- reason: "not_resumable_tail",
7702
- role: tail?.role,
7703
- stopReason: tail?.stopReason,
7704
- });
7705
- }
7797
+ this.#scheduleOverflowRetryContinuation(generation);
7706
7798
  } else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
7707
7799
  this.#scheduleAgentContinue({
7708
7800
  delayMs: 100,
@@ -7913,26 +8005,18 @@ export class AgentSession {
7913
8005
  details,
7914
8006
  preserveData,
7915
8007
  };
7916
- await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
8008
+ const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
8009
+ await this.#emitSessionEvent({
8010
+ type: "auto_compaction_end",
8011
+ action,
8012
+ result,
8013
+ aborted: false,
8014
+ willRetry: willRetry && !continuationSkipReason,
8015
+ continuationSkipReason,
8016
+ });
7917
8017
 
7918
8018
  if (willRetry) {
7919
- this.#stripOverflowFailedTurnForRetry();
7920
- if (!this.#isResumableAgentTail()) {
7921
- const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
7922
- logger.warn("Auto-compaction continuation skipped", {
7923
- source: "overflow_retry",
7924
- reason: "not_resumable_tail",
7925
- role: tail?.role,
7926
- stopReason: tail?.stopReason,
7927
- });
7928
- } else {
7929
- this.#scheduleAgentContinue({
7930
- delayMs: 100,
7931
- generation,
7932
- onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
7933
- onError: error => this.#logCompactionContinuationError("overflow_retry", error),
7934
- });
7935
- }
8019
+ this.#scheduleOverflowRetryContinuation(generation);
7936
8020
  } else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
7937
8021
  // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
7938
8022
  // Kick the loop so queued messages are actually delivered.
@@ -9089,6 +9173,63 @@ export class AgentSession {
9089
9173
  void this.#emitSessionEvent({ type: "irc_message", message: record });
9090
9174
  }
9091
9175
 
9176
+ emitSubagentSteerObservation(args: { from: string; to: string; body: string; timestamp?: number }): void {
9177
+ const timestamp = args.timestamp ?? Date.now();
9178
+ const observationId = crypto.randomUUID();
9179
+ const message: CustomMessage = {
9180
+ role: "custom",
9181
+ customType: "subagent:steer",
9182
+ content: `[Steer \`${args.from}\` ⇨ \`${args.to}\` (queued)]\n\n${args.body}`,
9183
+ display: true,
9184
+ details: { observationId, from: args.from, to: args.to, body: args.body, state: "queued" },
9185
+ attribution: "agent",
9186
+ timestamp,
9187
+ };
9188
+ void this.#emitSessionEvent({ type: "subagent_steer_message", message });
9189
+ this.#forwardSubagentSteerRelayToMain({
9190
+ from: args.from,
9191
+ to: args.to,
9192
+ body: args.body,
9193
+ observationId,
9194
+ timestamp,
9195
+ });
9196
+ }
9197
+
9198
+ #forwardSubagentSteerRelayToMain(args: {
9199
+ from: string;
9200
+ to: string;
9201
+ body: string;
9202
+ observationId: string;
9203
+ timestamp: number;
9204
+ }): void {
9205
+ const registry = this.#agentRegistry;
9206
+ if (!registry) return;
9207
+ if (this.#agentId === MAIN_AGENT_ID) return;
9208
+ const mainRef = registry.get(MAIN_AGENT_ID);
9209
+ const mainSession = mainRef?.session;
9210
+ if (!mainSession || mainSession === this) return;
9211
+ const record: CustomMessage = {
9212
+ role: "custom",
9213
+ customType: "subagent:steer:relay",
9214
+ content: `[Steer \`${args.from}\` ⇨ \`${args.to}\` (queued)]\n\n${args.body}`,
9215
+ display: true,
9216
+ details: {
9217
+ observationId: args.observationId,
9218
+ from: args.from,
9219
+ to: args.to,
9220
+ body: args.body,
9221
+ state: "queued",
9222
+ },
9223
+ attribution: "agent",
9224
+ timestamp: args.timestamp,
9225
+ };
9226
+ mainSession.emitSubagentSteerRelayObservation(record);
9227
+ }
9228
+
9229
+ emitSubagentSteerRelayObservation(record: CustomMessage): void {
9230
+ void this.#emitSessionEvent({ type: "subagent_steer_message", message: record });
9231
+ }
9232
+
9092
9233
  /**
9093
9234
  * Run a single ephemeral side-channel turn against this session's current
9094
9235
  * model + system prompt + history. No tools are used; the side request
@@ -7,6 +7,9 @@ export type {
7
7
  ApiKeyCredential,
8
8
  AuthCredential,
9
9
  AuthCredentialEntry,
10
+ AuthCredentialIfAbsentReason,
11
+ AuthCredentialIfAbsentResult,
12
+ AuthCredentialIfAbsentSnapshotResult,
10
13
  AuthCredentialStore,
11
14
  AuthStorageData,
12
15
  AuthStorageOptions,