@gajae-code/coding-agent 0.7.2 → 0.7.4

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 (154) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/bin/gjc.js +4 -0
  3. package/dist/types/cli/mcp-cli.d.ts +25 -0
  4. package/dist/types/cli/plugin-cli.d.ts +2 -0
  5. package/dist/types/cli.d.ts +6 -0
  6. package/dist/types/commands/mcp.d.ts +70 -0
  7. package/dist/types/commands/plugin.d.ts +6 -0
  8. package/dist/types/commands/session.d.ts +6 -0
  9. package/dist/types/config/keybindings.d.ts +2 -2
  10. package/dist/types/config/model-profile-activation.d.ts +8 -1
  11. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  12. package/dist/types/extensibility/gjc-plugins/compiler.d.ts +19 -0
  13. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +29 -0
  14. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  15. package/dist/types/extensibility/gjc-plugins/injection.d.ts +9 -0
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +13 -0
  17. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +26 -0
  18. package/dist/types/extensibility/gjc-plugins/observability.d.ts +27 -0
  19. package/dist/types/extensibility/gjc-plugins/prompt-appendix.d.ts +16 -0
  20. package/dist/types/extensibility/gjc-plugins/registry.d.ts +32 -0
  21. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +64 -0
  22. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +42 -0
  23. package/dist/types/extensibility/gjc-plugins/types.d.ts +158 -2
  24. package/dist/types/extensibility/gjc-plugins/validation.d.ts +8 -1
  25. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  26. package/dist/types/gjc-runtime/psmux-detect.d.ts +78 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -0
  28. package/dist/types/gjc-runtime/tmux-common.d.ts +20 -1
  29. package/dist/types/gjc-runtime/tmux-sessions.d.ts +18 -0
  30. package/dist/types/main.d.ts +2 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  32. package/dist/types/modes/components/model-selector.d.ts +8 -0
  33. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  34. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  35. package/dist/types/notifications/html-format.d.ts +11 -0
  36. package/dist/types/notifications/index.d.ts +149 -1
  37. package/dist/types/notifications/lifecycle-commands.d.ts +72 -0
  38. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  39. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  40. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  41. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  42. package/dist/types/notifications/recent-activity.d.ts +35 -0
  43. package/dist/types/notifications/telegram-daemon.d.ts +114 -16
  44. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  45. package/dist/types/notifications/topic-registry.d.ts +12 -9
  46. package/dist/types/runtime-mcp/types.d.ts +7 -0
  47. package/dist/types/sdk.d.ts +2 -0
  48. package/dist/types/session/agent-session.d.ts +14 -4
  49. package/dist/types/session/blob-store.d.ts +25 -0
  50. package/dist/types/session/session-manager.d.ts +57 -0
  51. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  52. package/dist/types/system-prompt.d.ts +2 -0
  53. package/dist/types/task/executor.d.ts +9 -1
  54. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  55. package/dist/types/tools/index.d.ts +3 -1
  56. package/dist/types/utils/changelog.d.ts +1 -0
  57. package/dist/types/web/insane/url-guard.d.ts +6 -3
  58. package/dist/types/web/scrapers/types.d.ts +5 -0
  59. package/dist/types/web/scrapers/utils.d.ts +7 -1
  60. package/package.json +11 -9
  61. package/scripts/g004-tmux-smoke.ts +100 -0
  62. package/scripts/g005-daemon-smoke.ts +181 -0
  63. package/scripts/g011-daemon-path-smoke.ts +153 -0
  64. package/src/cli/mcp-cli.ts +272 -0
  65. package/src/cli/plugin-cli.ts +66 -3
  66. package/src/cli.ts +27 -6
  67. package/src/commands/mcp.ts +117 -0
  68. package/src/commands/plugin.ts +4 -0
  69. package/src/commands/session.ts +18 -0
  70. package/src/config/keybindings.ts +2 -2
  71. package/src/config/model-profile-activation.ts +55 -7
  72. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  73. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +1 -1
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +7 -6
  75. package/src/defaults/gjc/skills/team/SKILL.md +5 -3
  76. package/src/defaults/gjc/skills/ultragoal/SKILL.md +41 -13
  77. package/src/export/html/index.ts +2 -2
  78. package/src/extensibility/extensions/runner.ts +1 -0
  79. package/src/extensibility/gjc-plugins/compiler.ts +351 -0
  80. package/src/extensibility/gjc-plugins/constrained-hooks.ts +170 -0
  81. package/src/extensibility/gjc-plugins/index.ts +9 -0
  82. package/src/extensibility/gjc-plugins/injection.ts +109 -0
  83. package/src/extensibility/gjc-plugins/installer.ts +434 -0
  84. package/src/extensibility/gjc-plugins/loader.ts +3 -1
  85. package/src/extensibility/gjc-plugins/mcp-policy.ts +239 -0
  86. package/src/extensibility/gjc-plugins/observability.ts +84 -0
  87. package/src/extensibility/gjc-plugins/paths.ts +1 -1
  88. package/src/extensibility/gjc-plugins/prompt-appendix.ts +109 -0
  89. package/src/extensibility/gjc-plugins/registry.ts +180 -0
  90. package/src/extensibility/gjc-plugins/runtime-adapters.ts +234 -0
  91. package/src/extensibility/gjc-plugins/schema.ts +250 -20
  92. package/src/extensibility/gjc-plugins/session-validation.ts +147 -0
  93. package/src/extensibility/gjc-plugins/types.ts +199 -3
  94. package/src/extensibility/gjc-plugins/validation.ts +80 -0
  95. package/src/extensibility/skills.ts +15 -0
  96. package/src/gjc-runtime/launch-tmux.ts +61 -7
  97. package/src/gjc-runtime/psmux-detect.ts +239 -0
  98. package/src/gjc-runtime/team-runtime.ts +56 -23
  99. package/src/gjc-runtime/tmux-common.ts +30 -3
  100. package/src/gjc-runtime/tmux-sessions.ts +51 -1
  101. package/src/gjc-runtime/ultragoal-guard.ts +25 -8
  102. package/src/gjc-runtime/ultragoal-runtime.ts +75 -15
  103. package/src/hooks/skill-state.ts +57 -0
  104. package/src/internal-urls/docs-index.generated.ts +12 -8
  105. package/src/main.ts +14 -3
  106. package/src/modes/bridge/bridge-mode.ts +11 -0
  107. package/src/modes/components/custom-editor.ts +2 -0
  108. package/src/modes/components/footer.ts +2 -3
  109. package/src/modes/components/hook-editor.ts +1 -1
  110. package/src/modes/components/hook-selector.ts +67 -43
  111. package/src/modes/components/model-selector.ts +56 -11
  112. package/src/modes/components/status-line/git-utils.ts +25 -0
  113. package/src/modes/components/status-line.ts +10 -11
  114. package/src/modes/components/welcome.ts +2 -3
  115. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  116. package/src/modes/controllers/selector-controller.ts +53 -11
  117. package/src/modes/interactive-mode.ts +4 -1
  118. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  119. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  120. package/src/modes/theme/defaults/index.ts +2 -0
  121. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  122. package/src/notifications/html-format.ts +38 -0
  123. package/src/notifications/index.ts +242 -12
  124. package/src/notifications/lifecycle-commands.ts +228 -0
  125. package/src/notifications/lifecycle-control-runtime.ts +400 -0
  126. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  127. package/src/notifications/operator-runtime.ts +171 -0
  128. package/src/notifications/rate-limit-pool.ts +19 -0
  129. package/src/notifications/recent-activity.ts +132 -0
  130. package/src/notifications/telegram-daemon.ts +778 -257
  131. package/src/notifications/telegram-reference.ts +25 -7
  132. package/src/notifications/topic-registry.ts +23 -9
  133. package/src/prompts/agents/executor.md +2 -2
  134. package/src/runtime-mcp/transports/stdio.ts +38 -4
  135. package/src/runtime-mcp/types.ts +7 -0
  136. package/src/sdk.ts +157 -10
  137. package/src/session/agent-session.ts +166 -74
  138. package/src/session/blob-store.ts +196 -8
  139. package/src/session/session-manager.ts +678 -7
  140. package/src/slash-commands/builtin-registry.ts +23 -3
  141. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  142. package/src/slash-commands/helpers/parse.ts +2 -1
  143. package/src/system-prompt.ts +9 -0
  144. package/src/task/executor.ts +31 -7
  145. package/src/task/index.ts +2 -0
  146. package/src/tools/ask.ts +5 -1
  147. package/src/tools/bash.ts +9 -0
  148. package/src/tools/composer-bash-policy.ts +96 -0
  149. package/src/tools/fetch.ts +18 -2
  150. package/src/tools/index.ts +3 -1
  151. package/src/utils/changelog.ts +8 -0
  152. package/src/web/insane/url-guard.ts +18 -14
  153. package/src/web/scrapers/types.ts +143 -45
  154. package/src/web/scrapers/utils.ts +70 -19
@@ -155,7 +155,7 @@ function getShellInputPrefix(isNoContext: boolean): string {
155
155
 
156
156
  function configureDefaultComposerChrome(editor: CustomEditor): void {
157
157
  editor.setBorderVisible(true);
158
- editor.setBorderStyle("sharp");
158
+ editor.setBorderStyle("round");
159
159
  editor.setClosedBorderBox(true);
160
160
  editor.setPromptGutter(undefined);
161
161
  editor.setInputPrefix(getDefaultInputPrefix());
@@ -431,6 +431,8 @@ export class InteractiveMode implements InteractiveModeContext {
431
431
  this.#resizeHandler = () => {
432
432
  this.#syncEditorMaxHeight();
433
433
  this.updateEditorChrome();
434
+ this.editor.invalidate();
435
+ this.ui.requestRender(true, "resize");
434
436
  };
435
437
  process.stdout.on("resize", this.#resizeHandler);
436
438
  try {
@@ -568,6 +570,7 @@ export class InteractiveMode implements InteractiveModeContext {
568
570
  this.ui.addChild(this.hookWidgetContainerAbove);
569
571
  this.ui.addChild(this.editorContainer);
570
572
  this.ui.addChild(this.hookWidgetContainerBelow);
573
+ this.ui.setBottomPinnedComponent(this.statusLine);
571
574
  this.ui.setFocus(this.editor);
572
575
 
573
576
  this.#inputController.setupKeyHandlers();
@@ -73,7 +73,7 @@ const RPC_COMMAND_SCOPE_REGISTRY: Record<RpcCommandType, BridgeCommandScope> = {
73
73
  get_login_providers: "admin",
74
74
  login: "admin",
75
75
  negotiate_unattended: "control",
76
- workflow_gate_response: "prompt",
76
+ workflow_gate_response: "control",
77
77
  };
78
78
 
79
79
  export const RPC_COMMAND_TYPES: readonly RpcCommandType[] = Object.keys(RPC_COMMAND_SCOPE_REGISTRY) as RpcCommandType[];
@@ -0,0 +1,99 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/can1357/gajae-code/main/packages/coding-agent/theme-schema.json",
3
+ "name": "gruvbox-dark",
4
+ "vars": {
5
+ "bgHard": "#1d2021",
6
+ "surface": "#32302f",
7
+ "surfaceBright": "#504945",
8
+ "borderNeutral": "#504945",
9
+ "borderSubtle": "#3c3836",
10
+ "fg": "#ebdbb2",
11
+ "muted": "#a89984",
12
+ "dim": "#7c6f64",
13
+ "gray": "#928374",
14
+ "red": "#fb4934",
15
+ "green": "#b8bb26",
16
+ "yellow": "#fabd2f",
17
+ "blue": "#83a598",
18
+ "purple": "#d3869b",
19
+ "aqua": "#8ec07c",
20
+ "orange": "#fe8019",
21
+ "diffRemovalRed": "#cc241d"
22
+ },
23
+ "colors": {
24
+ "accent": "orange",
25
+ "border": "borderNeutral",
26
+ "borderAccent": "orange",
27
+ "borderMuted": "borderSubtle",
28
+ "success": "green",
29
+ "error": "red",
30
+ "warning": "yellow",
31
+ "muted": "muted",
32
+ "dim": "dim",
33
+ "text": "fg",
34
+ "thinkingText": "muted",
35
+ "selectedBg": "surfaceBright",
36
+ "userMessageBg": "surface",
37
+ "userMessageText": "fg",
38
+ "customMessageBg": "surface",
39
+ "customMessageText": "fg",
40
+ "customMessageLabel": "orange",
41
+ "toolPendingBg": "surface",
42
+ "toolSuccessBg": "#283626",
43
+ "toolErrorBg": "#3c2323",
44
+ "toolTitle": "fg",
45
+ "toolOutput": "muted",
46
+ "mdHeading": "yellow",
47
+ "mdLink": "blue",
48
+ "mdLinkUrl": "muted",
49
+ "mdCode": "aqua",
50
+ "mdCodeBlock": "fg",
51
+ "mdCodeBlockBorder": "borderNeutral",
52
+ "mdQuote": "muted",
53
+ "mdQuoteBorder": "borderNeutral",
54
+ "mdHr": "dim",
55
+ "mdListBullet": "orange",
56
+ "toolDiffAdded": "green",
57
+ "toolDiffRemoved": "diffRemovalRed",
58
+ "toolDiffContext": "muted",
59
+ "syntaxComment": "gray",
60
+ "syntaxKeyword": "red",
61
+ "syntaxFunction": "green",
62
+ "syntaxVariable": "blue",
63
+ "syntaxString": "green",
64
+ "syntaxNumber": "purple",
65
+ "syntaxType": "yellow",
66
+ "syntaxOperator": "aqua",
67
+ "syntaxPunctuation": "muted",
68
+ "thinkingOff": "dim",
69
+ "thinkingMinimal": "muted",
70
+ "thinkingLow": "aqua",
71
+ "thinkingMedium": "yellow",
72
+ "thinkingHigh": "orange",
73
+ "thinkingXhigh": "red",
74
+ "bashMode": "green",
75
+ "pythonMode": "yellow",
76
+ "statusLineBg": "bgHard",
77
+ "statusLineSep": "dim",
78
+ "statusLineModel": "orange",
79
+ "statusLinePath": "blue",
80
+ "statusLineGitClean": "green",
81
+ "statusLineGitDirty": "yellow",
82
+ "statusLineContext": "aqua",
83
+ "statusLineSpend": "yellow",
84
+ "statusLineStaged": "green",
85
+ "statusLineDirty": "yellow",
86
+ "statusLineUntracked": "diffRemovalRed",
87
+ "statusLineOutput": "fg",
88
+ "statusLineCost": "orange",
89
+ "statusLineSubagents": "purple"
90
+ },
91
+ "export": {
92
+ "pageBg": "#1d2021",
93
+ "cardBg": "#282828",
94
+ "infoBg": "#32302f"
95
+ },
96
+ "symbols": {
97
+ "preset": "unicode"
98
+ }
99
+ }
@@ -1,6 +1,7 @@
1
1
  import blue_crab from "./blue-crab.json" with { type: "json" };
2
2
  import claude_code from "./claude-code.json" with { type: "json" };
3
3
  import codex from "./codex.json" with { type: "json" };
4
+ import gruvbox_dark from "./gruvbox-dark.json" with { type: "json" };
4
5
  import opencode from "./opencode.json" with { type: "json" };
5
6
  import red_claw from "./red-claw.json" with { type: "json" };
6
7
 
@@ -8,6 +9,7 @@ export const defaultThemes = {
8
9
  "blue-crab": blue_crab,
9
10
  "claude-code": claude_code,
10
11
  codex,
12
+ "gruvbox-dark": gruvbox_dark,
11
13
  opencode,
12
14
  "red-claw": red_claw,
13
15
  };
@@ -23,7 +23,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
23
23
  "|-----|--------|",
24
24
  "| `Enter` | Send / queue while busy |",
25
25
  `| \`${appKey(bindings, "app.message.queue")}\` | Queue message for next turn |`,
26
- "| `Shift+Enter` | New line |",
26
+ "| `Shift+Enter` / `Ctrl+J` | New line |",
27
27
  "| `Ctrl+W` / `Option+Backspace` | Delete word backwards |",
28
28
  "| `Ctrl+U` | Delete to start of line |",
29
29
  "| `Ctrl+K` | Delete to end of line |",
@@ -349,16 +349,54 @@ export function buttonLabel(label: string, index: number): string {
349
349
  return `${index + 1}. ${stripped}`;
350
350
  }
351
351
 
352
+ /** Numbered, escaped option list for the Telegram message body. */
353
+ export function numberedOptionList(labels: string[]): string {
354
+ return labels.map((label, i) => `${i + 1}. ${escapeHtml(label.replace(/^\s*\d+[.)]\s+/, ""))}`).join("\n");
355
+ }
356
+
357
+ /** Compact numeric button label; full option text belongs in the message body. */
358
+ export function choiceButtonLabel(index: number): string {
359
+ return String(index + 1);
360
+ }
361
+
352
362
  export interface InlineButton {
353
363
  text: string;
354
364
  callback_data: string;
355
365
  }
356
366
 
367
+ const COMPACT_BUTTONS_PER_ROW = 5;
368
+
357
369
  /** A prefixed button label is "long" when it is wide or contains a newline. */
358
370
  function isLongLabel(label: string): boolean {
359
371
  return label.length > 18 || /[\r\n]/.test(label);
360
372
  }
361
373
 
374
+ /**
375
+ * Lay out option callbacks as compact numeric buttons. Telegram mobile clients
376
+ * ellipsize long inline-keyboard labels and tall keyboards can be obscured by
377
+ * the composer, so the full choice text is rendered in the message body while
378
+ * the keyboard keeps only stable one-based tap targets.
379
+ */
380
+ export function buildCompactChoiceGrid(
381
+ labels: string[],
382
+ callbackForIndex: (index: number) => string,
383
+ ): InlineButton[][] {
384
+ const rows: InlineButton[][] = [];
385
+ let run: InlineButton[] = [];
386
+ const flush = () => {
387
+ if (run.length) {
388
+ rows.push(run);
389
+ run = [];
390
+ }
391
+ };
392
+ labels.forEach((_label, i) => {
393
+ run.push({ text: choiceButtonLabel(i), callback_data: callbackForIndex(i) });
394
+ if (run.length === COMPACT_BUTTONS_PER_ROW) flush();
395
+ });
396
+ flush();
397
+ return rows;
398
+ }
399
+
362
400
  /**
363
401
  * Lay out option labels as a numbered button grid. Long buttons take a
364
402
  * full-width row; runs of short buttons are packed into rows of up to 3. The
@@ -12,7 +12,7 @@
12
12
  * - `ask` (unattended/RPC): observes emitted workflow gates and resolves the real
13
13
  * gate on a remote reply via `ctx.workflowGate`.
14
14
  * - `turn_end` -> `action_needed` (kind `idle`, deduped per turn).
15
- * - `session_shutdown` -> stop the server + deregister the answer source.
15
+ * - `session_shutdown` -> `session_closed` frame, stop server, deregister answer source.
16
16
  *
17
17
  * Enable with Settings notifications config, `GJC_NOTIFICATIONS=1` (a token is
18
18
  * generated), or `GJC_NOTIFICATIONS_TOKEN`.
@@ -41,6 +41,193 @@ import {
41
41
  import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage } from "./helpers";
42
42
  import { ensureTelegramDaemonRunning } from "./telegram-daemon";
43
43
 
44
+ // ===========================================================================
45
+ // Session lifecycle control protocol (TypeScript mirror of the Rust wire
46
+ // contract in `crates/gjc-notifications/src/lifecycle.rs`).
47
+ //
48
+ // These describe the frames exchanged over the daemon-owned, session-independent
49
+ // control endpoint for remote session create / close / resume. Field names are
50
+ // camelCase on the wire; `type`/`kind` discriminators are snake_case. The Rust
51
+ // ingress authenticates and forwards; the daemon (TypeScript) owns all policy,
52
+ // spawn orchestration, idempotency, rate limiting, audit, and UX.
53
+ // ===========================================================================
54
+
55
+ /** Where a `session_create` should run. Discriminated by `kind`. */
56
+ export type SessionCreateTarget =
57
+ | { kind: "existing_path"; path: string }
58
+ | { kind: "worktree"; repo: string; branch: string }
59
+ | { kind: "plain_dir"; path: string };
60
+
61
+ /** Identifies the session a `session_close` targets. */
62
+ export interface SessionCloseTarget {
63
+ sessionId: string;
64
+ /** Expected GJC-managed tmux session name (defense-in-depth match). */
65
+ tmuxSession?: string;
66
+ /** Expected `@gjc-session-state-file` tag (defense-in-depth match). */
67
+ sessionStateFile?: string;
68
+ }
69
+
70
+ /** Identifies the session a `session_resume` targets. */
71
+ export interface SessionResumeTarget {
72
+ sessionIdOrPrefix: string;
73
+ /** Optional repo/working-dir hint to disambiguate matches. */
74
+ path?: string;
75
+ }
76
+
77
+ /** Create a new session. */
78
+ export interface SessionCreateFrame {
79
+ type: "session_create";
80
+ requestId: string;
81
+ /** Deterministic lifecycle marker preallocated by the daemon before spawn. */
82
+ lifecycleRequestId: string;
83
+ /** Session id the daemon preallocated and propagates to the child. */
84
+ intendedSessionId: string;
85
+ /** Telegram update id (idempotency key on the daemon side). */
86
+ updateId: number;
87
+ chatId: string;
88
+ /** Control-endpoint token authorizing this frame. */
89
+ token: string;
90
+ target: SessionCreateTarget;
91
+ /** Reference to the daemon-written, once-consumed startup-prompt file. */
92
+ startupPromptRef?: string;
93
+ }
94
+
95
+ /** Close (hard-kill, history preserved) a session. */
96
+ export interface SessionCloseFrame {
97
+ type: "session_close";
98
+ requestId: string;
99
+ updateId: number;
100
+ chatId: string;
101
+ token: string;
102
+ target: SessionCloseTarget;
103
+ /** Hard-kill even if a live pane is attached (GJC-managed only). */
104
+ force?: boolean;
105
+ }
106
+
107
+ /** Resume a session (reattach if alive, else cold-restart from history). */
108
+ export interface SessionResumeFrame {
109
+ type: "session_resume";
110
+ requestId: string;
111
+ updateId: number;
112
+ chatId: string;
113
+ token: string;
114
+ target: SessionResumeTarget;
115
+ startupPromptRef?: string;
116
+ }
117
+
118
+ /** Any client -> ingress lifecycle request frame. */
119
+ export type SessionLifecycleRequest = SessionCreateFrame | SessionCloseFrame | SessionResumeFrame;
120
+
121
+ /** Terminal status of a lifecycle request. */
122
+ export type LifecycleStatus = "ok" | "error";
123
+
124
+ /** A connected session's per-session endpoint, returned to the control client. */
125
+ export interface LifecycleEndpoint {
126
+ url: string;
127
+ token: string;
128
+ }
129
+
130
+ /** The Telegram topic/thread a session is surfaced in. */
131
+ export interface LifecycleTopic {
132
+ chatId: string;
133
+ threadId: string;
134
+ }
135
+
136
+ /** How a create request was correlated to its spawned session. */
137
+ export type MatchedBy = "spawn_marker" | "session_ready";
138
+
139
+ /** Response to a successful `session_create`. */
140
+ export interface SessionCreateResponseFrame {
141
+ type: "session_create_response";
142
+ requestId: string;
143
+ status: LifecycleStatus;
144
+ lifecycleRequestId: string;
145
+ sessionId: string;
146
+ matchedBy: MatchedBy;
147
+ endpoint: LifecycleEndpoint;
148
+ topic: LifecycleTopic;
149
+ target: SessionCreateTarget;
150
+ }
151
+
152
+ /** Response to a successful `session_close`. */
153
+ export interface SessionCloseResponseFrame {
154
+ type: "session_close_response";
155
+ requestId: string;
156
+ status: LifecycleStatus;
157
+ sessionId: string;
158
+ processGone: boolean;
159
+ historyPreserved: boolean;
160
+ endpointStale: boolean;
161
+ }
162
+
163
+ /** Whether a resume reattached to a live session or cold-restarted a dead one. */
164
+ export type ResumeMode = "reattached" | "cold_restarted";
165
+
166
+ /** Response to a successful `session_resume`. */
167
+ export interface SessionResumeResponseFrame {
168
+ type: "session_resume_response";
169
+ requestId: string;
170
+ status: LifecycleStatus;
171
+ sessionId: string;
172
+ mode: ResumeMode;
173
+ endpoint: LifecycleEndpoint;
174
+ topic: LifecycleTopic;
175
+ }
176
+
177
+ /** Machine-readable reason a lifecycle request failed. */
178
+ export type LifecycleErrorReason =
179
+ | "unauthorized"
180
+ | "rate_limited"
181
+ | "duplicate_conflict"
182
+ | "invalid_target"
183
+ | "ambiguous_target"
184
+ | "spawn_failed"
185
+ | "discovery_timeout"
186
+ | "readiness_timeout"
187
+ | "close_refused"
188
+ | "not_found"
189
+ | "terminal_uncertain";
190
+
191
+ /** A candidate returned with an `ambiguous_target` resume error. */
192
+ export interface ResumeCandidate {
193
+ sessionId: string;
194
+ path?: string;
195
+ /** Last-activity epoch-millis (session history file mtime), if known. */
196
+ mtimeMs?: number;
197
+ }
198
+
199
+ /** A structured lifecycle error frame. */
200
+ export interface SessionLifecycleErrorFrame {
201
+ type: "session_lifecycle_error";
202
+ requestId: string;
203
+ status: LifecycleStatus;
204
+ reason: LifecycleErrorReason;
205
+ message: string;
206
+ candidates?: ResumeCandidate[];
207
+ }
208
+
209
+ /** Any ingress -> client lifecycle response frame. */
210
+ export type SessionLifecycleResponse =
211
+ | SessionCreateResponseFrame
212
+ | SessionCloseResponseFrame
213
+ | SessionResumeResponseFrame
214
+ | SessionLifecycleErrorFrame;
215
+
216
+ /**
217
+ * Replayable per-session readiness signal (mirror of the Rust `session_ready`
218
+ * frame). Buffered and replayed to late clients so WS-open alone never implies
219
+ * the session is live and surfaced.
220
+ */
221
+ export interface SessionReadyFrame {
222
+ type: "session_ready";
223
+ sessionId: string;
224
+ lifecycleRequestId?: string;
225
+ startupPromptRef?: string;
226
+ repo?: string;
227
+ branch?: string;
228
+ title?: string;
229
+ }
230
+
44
231
  /** Resolve the git dir for `cwd`, handling worktrees where `.git` is a file. */
45
232
  function gitDir(cwd: string): string | undefined {
46
233
  const dot = path.join(cwd, ".git");
@@ -141,6 +328,7 @@ interface SessionRuntime {
141
328
  /** Deregisters this session's Telegram file sink. */
142
329
  disposeFileSink: () => void;
143
330
  redact: boolean;
331
+ verbosity: "lean" | "verbose";
144
332
  sessionTag: string;
145
333
  /** Whether the agent loop is currently running (drives the typing indicator). */
146
334
  busy: boolean;
@@ -248,7 +436,7 @@ function registerInteractiveAnswerSource(
248
436
  id: string,
249
437
  server: NotificationServer,
250
438
  pendingInteractive: Map<string, PendingInteractiveAsk>,
251
- redact: boolean,
439
+ getRedact: () => boolean,
252
440
  tag: string,
253
441
  ): () => void {
254
442
  return registerAskAnswerSource(id, {
@@ -260,7 +448,7 @@ function registerInteractiveAnswerSource(
260
448
  JSON.stringify(
261
449
  notificationActionPayload(
262
450
  { id: askId, kind: "ask", sessionId: id, question, options },
263
- { redact, sessionTag: tag },
451
+ { redact: getRedact(), sessionTag: tag },
264
452
  ),
265
453
  ),
266
454
  true,
@@ -296,8 +484,9 @@ export const createNotificationsExtension: ExtensionFactory = api => {
296
484
  const runtimes = new Map<string, SessionRuntime>();
297
485
  const disabledSessions = new Set<string>();
298
486
  const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
487
+ const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
299
488
 
300
- function stopSession(id: string): boolean {
489
+ async function stopSession(id: string): Promise<boolean> {
301
490
  const rt = runtimes.get(id);
302
491
  if (!rt) return false;
303
492
  runtimes.delete(id);
@@ -308,6 +497,14 @@ export const createNotificationsExtension: ExtensionFactory = api => {
308
497
  // Resolve any still-pending interactive asks so the ask tool is not left hanging.
309
498
  for (const pending of rt.pendingInteractive.values()) pending.resolve(undefined);
310
499
  rt.pendingInteractive.clear();
500
+ let closeFrameSent = false;
501
+ try {
502
+ rt.server.pushFrame(JSON.stringify({ type: "session_closed", sessionId: id }));
503
+ closeFrameSent = true;
504
+ } catch (e) {
505
+ logger.warn(`notifications: session_closed failed: ${String(e)}`);
506
+ }
507
+ if (closeFrameSent) await sleep(100);
311
508
  try {
312
509
  rt.server.stop();
313
510
  } catch (e) {
@@ -336,6 +533,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
336
533
  const pendingInteractive = new Map<string, PendingInteractiveAsk>();
337
534
  const tag = sessionTag(id);
338
535
  const redact = cfg.redact;
536
+ const verbosity = cfg.verbosity;
339
537
  let runtime: SessionRuntime | undefined;
340
538
 
341
539
  // The SDK can always answer now (interactive via the answer source, or the
@@ -410,7 +608,31 @@ export const createNotificationsExtension: ExtensionFactory = api => {
410
608
  return;
411
609
  }
412
610
  if (inbound.kind === "config_command") {
413
- if (runtime && typeof inbound.redact === "boolean") runtime.redact = inbound.redact;
611
+ if (!runtime) return;
612
+ const update: {
613
+ type: "config_update";
614
+ sessionId: string;
615
+ verbosity?: "lean" | "verbose";
616
+ redact?: boolean;
617
+ } = {
618
+ type: "config_update",
619
+ sessionId: id,
620
+ };
621
+ if (inbound.verbosity === "lean" || inbound.verbosity === "verbose") {
622
+ runtime.verbosity = inbound.verbosity;
623
+ update.verbosity = inbound.verbosity;
624
+ }
625
+ if (typeof inbound.redact === "boolean") {
626
+ runtime.redact = inbound.redact;
627
+ update.redact = inbound.redact;
628
+ }
629
+ if (update.verbosity !== undefined || update.redact !== undefined) {
630
+ try {
631
+ runtime.server.pushFrame(JSON.stringify(update));
632
+ } catch (e) {
633
+ logger.warn(`notifications: config_update failed: ${String(e)}`);
634
+ }
635
+ }
414
636
  }
415
637
  });
416
638
 
@@ -418,7 +640,13 @@ export const createNotificationsExtension: ExtensionFactory = api => {
418
640
  const endpoint = await server.start();
419
641
 
420
642
  // Interactive answer source: the ask tool races the local UI against this.
421
- const disposeAnswerSource = registerInteractiveAnswerSource(id, server, pendingInteractive, redact, tag);
643
+ const disposeAnswerSource = registerInteractiveAnswerSource(
644
+ id,
645
+ server,
646
+ pendingInteractive,
647
+ () => runtime?.redact ?? redact,
648
+ tag,
649
+ );
422
650
  const disposeFileSink = registerTelegramFileSink(id, async file => {
423
651
  try {
424
652
  const data = await fs.promises.readFile(file.path);
@@ -444,6 +672,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
444
672
  disposeAnswerSource,
445
673
  disposeFileSink,
446
674
  redact,
675
+ verbosity,
447
676
  sessionTag: tag,
448
677
  busy: false,
449
678
  pendingInbound: new Set<number>(),
@@ -519,7 +748,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
519
748
 
520
749
  if (command === "off") {
521
750
  disabledSessions.add(id);
522
- const stopped = stopSession(id);
751
+ const stopped = await stopSession(id);
523
752
  ctx.ui.notify(
524
753
  stopped
525
754
  ? "Notifications disabled for this session."
@@ -567,8 +796,9 @@ export const createNotificationsExtension: ExtensionFactory = api => {
567
796
  const running = runtimes.has(id);
568
797
  const locallyDisabled = disabledSessions.has(id);
569
798
  const enabled = isEnabledForSession(id, resolved.cfg);
799
+ const runtime = runtimes.get(id);
570
800
  ctx.ui.notify(
571
- `Notifications ${running ? "running" : enabled ? "enabled" : "disabled"} for this session; redaction ${resolved.cfg.redact ? "on" : "off"}${locallyDisabled ? "; locally off" : ""}.`,
801
+ `Notifications ${running ? "running" : enabled ? "enabled" : "disabled"} for this session; redaction ${(runtime?.redact ?? resolved.cfg.redact) ? "on" : "off"}; verbosity ${runtime?.verbosity ?? resolved.cfg.verbosity}${locallyDisabled ? "; locally off" : ""}.`,
572
802
  "info",
573
803
  );
574
804
  },
@@ -616,7 +846,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
616
846
  newId,
617
847
  rt.server,
618
848
  rt.pendingInteractive,
619
- rt.redact,
849
+ () => rt.redact,
620
850
  rt.sessionTag,
621
851
  );
622
852
  rt.disposeFileSink = registerTelegramFileSink(newId, async file => {
@@ -735,7 +965,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
735
965
  // On idle, stream a context update with metadata (token/model usage +
736
966
  // working-tree diff) unless redaction is on. The agent's last message is
737
967
  // NOT repeated here — it is already streamed once via `turn_stream`.
738
- if (!rt.redact) {
968
+ if (!rt.redact && rt.verbosity === "verbose") {
739
969
  const usage = (
740
970
  ctx as { getContextUsage?: () => { tokens: number | null; contextWindow: number } | undefined }
741
971
  ).getContextUsage?.();
@@ -836,7 +1066,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
836
1066
  }
837
1067
  });
838
1068
 
839
- api.on("session_shutdown", (_event, ctx) => {
840
- stopSession(sessionId(ctx));
1069
+ api.on("session_shutdown", async (_event, ctx) => {
1070
+ await stopSession(sessionId(ctx));
841
1071
  });
842
1072
  };