@gajae-code/coding-agent 0.7.3 → 0.7.5

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 (118) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bin/gjc.js +4 -0
  3. package/dist/types/cli/plugin-cli.d.ts +2 -0
  4. package/dist/types/commands/plugin.d.ts +6 -0
  5. package/dist/types/commands/session.d.ts +6 -0
  6. package/dist/types/config/model-profile-activation.d.ts +8 -1
  7. package/dist/types/extensibility/gjc-plugins/compiler.d.ts +19 -0
  8. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +29 -0
  9. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  10. package/dist/types/extensibility/gjc-plugins/injection.d.ts +9 -0
  11. package/dist/types/extensibility/gjc-plugins/installer.d.ts +13 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +26 -0
  13. package/dist/types/extensibility/gjc-plugins/observability.d.ts +27 -0
  14. package/dist/types/extensibility/gjc-plugins/prompt-appendix.d.ts +16 -0
  15. package/dist/types/extensibility/gjc-plugins/registry.d.ts +32 -0
  16. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +64 -0
  17. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +42 -0
  18. package/dist/types/extensibility/gjc-plugins/types.d.ts +158 -2
  19. package/dist/types/extensibility/gjc-plugins/validation.d.ts +8 -1
  20. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  21. package/dist/types/gjc-runtime/psmux-detect.d.ts +78 -0
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -0
  23. package/dist/types/gjc-runtime/tmux-common.d.ts +30 -2
  24. package/dist/types/gjc-runtime/tmux-sessions.d.ts +18 -0
  25. package/dist/types/main.d.ts +2 -0
  26. package/dist/types/modes/components/model-selector.d.ts +6 -0
  27. package/dist/types/notifications/html-format.d.ts +11 -0
  28. package/dist/types/notifications/index.d.ts +149 -1
  29. package/dist/types/notifications/lifecycle-commands.d.ts +72 -0
  30. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  31. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  32. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  33. package/dist/types/notifications/recent-activity.d.ts +35 -0
  34. package/dist/types/notifications/telegram-daemon.d.ts +60 -0
  35. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  36. package/dist/types/notifications/topic-registry.d.ts +10 -9
  37. package/dist/types/runtime-mcp/types.d.ts +7 -0
  38. package/dist/types/sdk.d.ts +2 -0
  39. package/dist/types/session/agent-session.d.ts +14 -4
  40. package/dist/types/session/blob-store.d.ts +25 -0
  41. package/dist/types/session/session-manager.d.ts +57 -0
  42. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  43. package/dist/types/system-prompt.d.ts +2 -0
  44. package/dist/types/task/executor.d.ts +9 -1
  45. package/dist/types/tools/index.d.ts +3 -1
  46. package/dist/types/utils/changelog.d.ts +1 -0
  47. package/package.json +11 -9
  48. package/scripts/g004-tmux-smoke.ts +100 -0
  49. package/scripts/g005-daemon-smoke.ts +181 -0
  50. package/scripts/g011-daemon-path-smoke.ts +153 -0
  51. package/src/cli/plugin-cli.ts +66 -3
  52. package/src/cli.ts +21 -4
  53. package/src/commands/plugin.ts +4 -0
  54. package/src/commands/session.ts +18 -0
  55. package/src/config/model-profile-activation.ts +55 -7
  56. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +1 -1
  57. package/src/defaults/gjc/skills/deep-interview/SKILL.md +3 -3
  58. package/src/defaults/gjc/skills/team/SKILL.md +5 -4
  59. package/src/defaults/gjc/skills/ultragoal/SKILL.md +41 -13
  60. package/src/export/html/index.ts +2 -2
  61. package/src/extensibility/gjc-plugins/compiler.ts +351 -0
  62. package/src/extensibility/gjc-plugins/constrained-hooks.ts +170 -0
  63. package/src/extensibility/gjc-plugins/index.ts +9 -0
  64. package/src/extensibility/gjc-plugins/injection.ts +109 -0
  65. package/src/extensibility/gjc-plugins/installer.ts +434 -0
  66. package/src/extensibility/gjc-plugins/loader.ts +3 -1
  67. package/src/extensibility/gjc-plugins/mcp-policy.ts +239 -0
  68. package/src/extensibility/gjc-plugins/observability.ts +84 -0
  69. package/src/extensibility/gjc-plugins/paths.ts +1 -1
  70. package/src/extensibility/gjc-plugins/prompt-appendix.ts +109 -0
  71. package/src/extensibility/gjc-plugins/registry.ts +180 -0
  72. package/src/extensibility/gjc-plugins/runtime-adapters.ts +234 -0
  73. package/src/extensibility/gjc-plugins/schema.ts +250 -20
  74. package/src/extensibility/gjc-plugins/session-validation.ts +147 -0
  75. package/src/extensibility/gjc-plugins/types.ts +199 -3
  76. package/src/extensibility/gjc-plugins/validation.ts +80 -0
  77. package/src/extensibility/skills.ts +15 -0
  78. package/src/gjc-runtime/launch-tmux.ts +58 -15
  79. package/src/gjc-runtime/psmux-detect.ts +239 -0
  80. package/src/gjc-runtime/team-runtime.ts +56 -23
  81. package/src/gjc-runtime/tmux-common.ts +85 -3
  82. package/src/gjc-runtime/tmux-sessions.ts +111 -9
  83. package/src/gjc-runtime/ultragoal-runtime.ts +75 -15
  84. package/src/internal-urls/docs-index.generated.ts +5 -4
  85. package/src/main.ts +14 -3
  86. package/src/modes/components/assistant-message.ts +49 -1
  87. package/src/modes/components/hook-editor.ts +1 -1
  88. package/src/modes/components/hook-selector.ts +67 -43
  89. package/src/modes/components/model-selector.ts +44 -11
  90. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  91. package/src/modes/controllers/selector-controller.ts +50 -11
  92. package/src/modes/interactive-mode.ts +2 -0
  93. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  94. package/src/notifications/html-format.ts +38 -0
  95. package/src/notifications/index.ts +242 -12
  96. package/src/notifications/lifecycle-commands.ts +228 -0
  97. package/src/notifications/lifecycle-control-runtime.ts +400 -0
  98. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  99. package/src/notifications/rate-limit-pool.ts +19 -0
  100. package/src/notifications/recent-activity.ts +132 -0
  101. package/src/notifications/telegram-daemon.ts +433 -8
  102. package/src/notifications/telegram-reference.ts +25 -7
  103. package/src/notifications/topic-registry.ts +18 -9
  104. package/src/prompts/agents/executor.md +2 -2
  105. package/src/runtime-mcp/transports/stdio.ts +38 -4
  106. package/src/runtime-mcp/types.ts +7 -0
  107. package/src/sdk.ts +157 -10
  108. package/src/session/agent-session.ts +166 -74
  109. package/src/session/blob-store.ts +196 -8
  110. package/src/session/session-manager.ts +739 -12
  111. package/src/slash-commands/builtin-registry.ts +23 -3
  112. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  113. package/src/system-prompt.ts +9 -0
  114. package/src/task/executor.ts +31 -7
  115. package/src/task/index.ts +2 -0
  116. package/src/tools/ask.ts +5 -1
  117. package/src/tools/index.ts +3 -1
  118. package/src/utils/changelog.ts +8 -0
@@ -0,0 +1,144 @@
1
+ import type { LifecycleErrorReason, ResumeCandidate, SessionCreateFrame, SessionLifecycleRequest } from "./index";
2
+ /** Durable idempotency state for a single lifecycle request. */
3
+ export type LedgerState = "in_progress" | "success" | "failure" | "terminal_uncertain";
4
+ /** One persisted idempotency entry, keyed by `chatId:updateId`. */
5
+ export interface LedgerEntry {
6
+ requestHash: string;
7
+ state: LedgerState;
8
+ requestId: string;
9
+ verb: SessionLifecycleRequest["type"];
10
+ intendedSessionId?: string;
11
+ startupPromptRef?: string;
12
+ createdAt: number;
13
+ updatedAt: number;
14
+ targetSummary: Record<string, unknown>;
15
+ sessionId?: string;
16
+ tmuxSession?: string;
17
+ sessionStateFile?: string;
18
+ endpointUrl?: string;
19
+ /** Close effect outcome: whether the tmux process is confirmed gone. */
20
+ processGone?: boolean;
21
+ reason?: LifecycleErrorReason;
22
+ }
23
+ /** The full on-disk ledger document. */
24
+ export interface LedgerDoc {
25
+ version: 1;
26
+ entries: Record<string, LedgerEntry>;
27
+ }
28
+ /** Persistence boundary: atomic + fsynced read/write of the ledger document. */
29
+ export interface LedgerStore {
30
+ read(): Promise<LedgerDoc>;
31
+ /** Write atomically (temp + fsync + rename) under a per-ledger lock. */
32
+ write(doc: LedgerDoc): Promise<void>;
33
+ }
34
+ /** One audit line. Tokens and raw prompts are NEVER included. */
35
+ export interface AuditEvent {
36
+ ts: string;
37
+ event: "accepted" | "rejected" | "duplicate_reack" | "rate_limited" | "spawn_started" | "recovered_in_progress" | "success" | "failure" | "terminal_uncertain";
38
+ chatId: string;
39
+ updateId: number;
40
+ requestId: string;
41
+ requestHash: string;
42
+ verb: SessionLifecycleRequest["type"];
43
+ targetSummary: Record<string, unknown>;
44
+ sessionId?: string;
45
+ tmuxSession?: string;
46
+ reason?: LifecycleErrorReason;
47
+ /** Prompt byte length only (never the prompt text). */
48
+ promptBytes?: number;
49
+ /** Prompt content hash only (never the prompt text). */
50
+ promptHash?: string;
51
+ }
52
+ export interface CreateEffectResult {
53
+ sessionId: string;
54
+ tmuxSession: string;
55
+ sessionStateFile?: string;
56
+ endpointUrl: string;
57
+ topicThreadId: string;
58
+ }
59
+ export interface ResumeEffectResult extends CreateEffectResult {
60
+ mode: "reattached" | "cold_restarted";
61
+ }
62
+ /** Injected effects + policy. Pure orchestration calls into these. */
63
+ export interface OrchestratorDeps {
64
+ /** The single paired chat id. Anything else is rejected before parsing. */
65
+ pairedChatId: string;
66
+ now: () => number;
67
+ store: LedgerStore;
68
+ audit: (event: AuditEvent) => Promise<void> | void;
69
+ /** Per-chat create rate limiter: returns true when allowed. */
70
+ allowCreate: (chatId: string, nowMs: number) => boolean;
71
+ /** Persist the once-consumed 0600 startup-prompt file; returns its ref. */
72
+ writeStartupPrompt: (requestId: string, prompt: string | undefined) => Promise<string | undefined>;
73
+ /** Spawn a session for a create/cold-restart. */
74
+ spawnCreate: (frame: SessionCreateFrame, ids: {
75
+ lifecycleRequestId: string;
76
+ intendedSessionId: string;
77
+ startupPromptRef?: string;
78
+ }) => Promise<CreateEffectResult>;
79
+ closeSession: (target: {
80
+ sessionId: string;
81
+ tmuxSession?: string;
82
+ sessionStateFile?: string;
83
+ }) => Promise<{
84
+ processGone: boolean;
85
+ }>;
86
+ resumeSession: (target: {
87
+ sessionIdOrPrefix: string;
88
+ path?: string;
89
+ }) => Promise<ResumeEffectResult | {
90
+ ambiguous: ResumeCandidate[];
91
+ } | {
92
+ notFound: true;
93
+ }>;
94
+ newLifecycleRequestId: () => string;
95
+ newSessionId: () => string;
96
+ }
97
+ /** A redaction-safe summary of a request target (never includes the token). */
98
+ export declare function summarizeTarget(frame: SessionLifecycleRequest): Record<string, unknown>;
99
+ /**
100
+ * Stable request hash over the meaningful (non-token) request content. Used to
101
+ * detect a duplicate update id reused with a DIFFERENT body (conflict).
102
+ */
103
+ export declare function requestHash(frame: SessionLifecycleRequest): string;
104
+ export declare function ledgerKey(chatId: string, updateId: number): string;
105
+ /** How a freshly-arrived request relates to the durable ledger. */
106
+ export type DuplicateClass = {
107
+ kind: "new";
108
+ } | {
109
+ kind: "reack_success";
110
+ entry: LedgerEntry;
111
+ } | {
112
+ kind: "reack_failure";
113
+ entry: LedgerEntry;
114
+ } | {
115
+ kind: "in_progress";
116
+ entry: LedgerEntry;
117
+ } | {
118
+ kind: "terminal_uncertain";
119
+ entry: LedgerEntry;
120
+ } | {
121
+ kind: "conflict";
122
+ entry: LedgerEntry;
123
+ };
124
+ /** Classify a request against an existing ledger entry (pure). */
125
+ export declare function classifyDuplicate(existing: LedgerEntry | undefined, hash: string): DuplicateClass;
126
+ /** The structured outcome the daemon translates into a wire response frame. */
127
+ export type LifecycleOutcome = {
128
+ status: "ok";
129
+ entry: LedgerEntry;
130
+ mode?: "reattached" | "cold_restarted";
131
+ } | {
132
+ status: "error";
133
+ reason: LifecycleErrorReason;
134
+ message: string;
135
+ candidates?: ResumeCandidate[];
136
+ } | {
137
+ status: "pending";
138
+ entry: LedgerEntry;
139
+ };
140
+ /**
141
+ * Handle one authenticated lifecycle request. Enforces paired-chat gating,
142
+ * idempotency, and rate limiting BEFORE any side effect, then dispatches.
143
+ */
144
+ export declare function handleLifecycleRequest(frame: SessionLifecycleRequest, deps: OrchestratorDeps): Promise<LifecycleOutcome>;
@@ -81,6 +81,8 @@ export declare class RateLimitPool<T = unknown> {
81
81
  * single session monopolises a lane), consuming one token each.
82
82
  */
83
83
  drain(nowMs?: number): RateLimitItem<T>[];
84
+ /** Remove queued items matching `predicate` without consuming tokens. Returns removed items in lane/FIFO order. */
85
+ removeWhere(predicate: (item: RateLimitItem<T>) => boolean): RateLimitItem<T>[];
84
86
  private refill;
85
87
  /** Pop the next item by lane priority + per-session round-robin fairness. */
86
88
  private takeNext;
@@ -0,0 +1,35 @@
1
+ /** One ranked recent-session entry surfaced to the picker. */
2
+ export interface RecentSessionEntry {
3
+ /** Session id (the `.jsonl` file stem). */
4
+ sessionId: string;
5
+ /** Working directory / repo path, when recoverable from the header. */
6
+ path?: string;
7
+ /** Branch, when recoverable from the header. */
8
+ branch?: string;
9
+ /** A short title (first user message), when recoverable. */
10
+ title?: string;
11
+ /** Absolute path of the session history (state) file. */
12
+ sessionStateFile: string;
13
+ /** Last-activity epoch-millis (history file mtime). */
14
+ mtimeMs: number;
15
+ /** True when a terminal breadcrumb points at this session file. */
16
+ currentTerminal?: boolean;
17
+ }
18
+ export interface RecentActivityDeps {
19
+ /** Root holding `<encoded-cwd>/<sessionId>.jsonl` history files. */
20
+ sessionsRoot: string;
21
+ /** Optional breadcrumb session-file paths (current terminals). */
22
+ breadcrumbPaths?: string[];
23
+ /** Max entries to return (default 20). */
24
+ limit?: number;
25
+ /** Injection seam for tests. */
26
+ readFirstLine?: (file: string) => string | undefined;
27
+ }
28
+ /**
29
+ * List recent sessions ranked by history-file mtime (newest first).
30
+ *
31
+ * Scans `<sessionsRoot>/<encoded-cwd>/<sessionId>.jsonl`, stats each file, and
32
+ * returns up to `limit` entries enriched with header metadata and a
33
+ * `currentTerminal` flag for any breadcrumb-referenced session file.
34
+ */
35
+ export declare function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry[];
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import type { Settings } from "../config/settings";
3
3
  import type { DaemonRuntimeInfo } from "../daemon/control-types";
4
+ import { type ControlServerLike, type LifecycleControlServerFactory } from "./lifecycle-control-runtime";
4
5
  import { NotificationOperatorRuntime, OperatorBackoffPolicy } from "./operator-runtime";
5
6
  import { type AliasTable, type CallbackRoute, type PendingAsk } from "./telegram-reference";
6
7
  export type EnsureDaemonResult = "owner_spawned" | "attached" | "disabled";
@@ -59,6 +60,22 @@ export declare const CLIENT_PING_PONG_CAPABILITY = "client_ping_pong";
59
60
  /** Protocol version the daemon advertises in its ClientHello. */
60
61
  export declare const NOTIFICATION_PROTOCOL_VERSION = 2;
61
62
  export declare function daemonPaths(agentDir: string): DaemonPaths;
63
+ /**
64
+ * Attach session-lifecycle control (create/close/resume) to the running daemon.
65
+ *
66
+ * Wires an already-started, authenticated control server to the lifecycle
67
+ * orchestrator with real daemon-side effects (tmux launcher / force-close /
68
+ * resume), a durable fsynced idempotency ledger + audit JSONL under the agent
69
+ * notifications dir, and strict paired-chat gating. The control server itself
70
+ * (NotificationControlServer) is owned/started by the daemon process; this
71
+ * function only connects it to policy. Returns the orchestrator deps for tests.
72
+ */
73
+ export declare function startDaemonLifecycleControl(input: {
74
+ controlServer: ControlServerLike;
75
+ pairedChatId: string;
76
+ agentDir: string;
77
+ env?: NodeJS.ProcessEnv;
78
+ }): void;
62
79
  export declare function registerNotificationRoot(input: {
63
80
  settings: Settings;
64
81
  cwd: string;
@@ -209,8 +226,17 @@ export interface TelegramDaemonOptions {
209
226
  idleTimeoutMs?: number;
210
227
  scanIntervalMs?: number;
211
228
  pid?: number;
229
+ /** Liveness probe for skipping dead-PID endpoint records in {@link TelegramNotificationDaemon.scanRoots}. */
230
+ pidAlive?: (pid: number) => boolean;
212
231
  botApi?: BotApi;
213
232
  control?: DaemonControlHooks;
233
+ /**
234
+ * Factory for the session-lifecycle control server. Defaults to the real
235
+ * native NotificationControlServer; tests inject a fake to verify the
236
+ * owner-bound start/stop lifecycle without a socket. When `undefined` AND no
237
+ * default applies (e.g. lifecycle control disabled), no control server starts.
238
+ */
239
+ createLifecycleControlServer?: LifecycleControlServerFactory | null;
214
240
  }
215
241
  interface SessionSocket {
216
242
  sessionId: string;
@@ -259,12 +285,45 @@ export declare class TelegramNotificationDaemon {
259
285
  private get busy();
260
286
  /** Inbound update id → originating Telegram message, for delivery reactions. */
261
287
  private get inboundReactions();
288
+ /**
289
+ * The owner-bound session-lifecycle control server (create/close/resume).
290
+ * Started in {@link run} after ownership is confirmed (so exactly one owner
291
+ * ever runs one), stopped in run()'s finally on any exit path.
292
+ */
293
+ private controlServer;
294
+ /** True while lifecycle control is active, so the loop keeps polling at idle. */
295
+ private lifecycleControlActive;
296
+ /** Control token (in-memory) the loopback client presents; never persisted/logged. */
297
+ private controlToken;
298
+ /** Loopback WS client to the daemon's own control endpoint (Option A real wire path). */
299
+ private controlClient;
300
+ /** Pending lifecycle responses awaiting a control-endpoint reply, by requestId. */
301
+ private readonly pendingLifecycle;
302
+ /** Monotonic counter for unique lifecycle request ids. */
303
+ private lifecycleSeq;
262
304
  /**
263
305
  * Cooperatively stop the daemon: set the stop flag and abort the in-flight
264
306
  * long poll so the run loop wakes immediately instead of waiting out the
265
307
  * ~25s getUpdates timeout. Safe to call from a signal handler.
266
308
  */
267
309
  requestStop(_reason?: "reload" | "stop" | "signal"): void;
310
+ private startLifecycleControl;
311
+ /** Stop the lifecycle control server (idempotent); called from run()'s finally. */
312
+ private stopLifecycleControl;
313
+ /**
314
+ * Connect the loopback control client and resolve responses by requestId.
315
+ * Resolves true once the socket is OPEN (bounded), false on error/timeout, so
316
+ * the caller only marks lifecycle control active when commands can be sent.
317
+ */
318
+ private connectControlClient;
319
+ /** Send a lifecycle frame over the loopback client and await the response. */
320
+ private submitLifecycleFrame;
321
+ private nextLifecycleRequestId;
322
+ /** Build an authenticated lifecycle frame from a parsed command + identity. */
323
+ private buildLifecycleFrame;
324
+ private handleLifecycleCommand;
325
+ /** Map a lifecycle response/error to a user-facing message (G010 surfacing). */
326
+ private formatLifecycleResponse;
268
327
  constructor(opts: TelegramDaemonOptions);
269
328
  private createSessionRouter;
270
329
  loadAliases(): Promise<void>;
@@ -295,6 +354,7 @@ export declare class TelegramNotificationDaemon {
295
354
  private rememberPendingThreadedFrame;
296
355
  private flushPendingThreadedFrames;
297
356
  private ensureTopic;
357
+ private deleteTopic;
298
358
  private persistTopics;
299
359
  loadTopics(): Promise<void>;
300
360
  private downloadTelegramFile;
@@ -90,10 +90,12 @@ export interface RouteInboundContext {
90
90
  }
91
91
  /** Route a Telegram update to a session/action without I/O. Fail closed under ambiguity. */
92
92
  export declare function routeInboundUpdate(update: unknown, ctx: RouteInboundContext): RouteDecision;
93
- /** Read `{url, token}` from an endpoint discovery file. */
93
+ /** Read `{url, token, pid?, stale?}` from an endpoint discovery file. */
94
94
  export declare function readEndpoint(path: string): {
95
95
  url: string;
96
96
  token: string;
97
+ pid?: number;
98
+ stale?: boolean;
97
99
  };
98
100
  /** Options for {@link runTelegramReferenceClient}. */
99
101
  export interface TelegramReferenceOptions {
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Per-session forum-topic registry for the threaded session surface.
3
3
  *
4
- * Each GJC 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
4
+ * Each GJC session owns one active Telegram forum topic in the paired private
5
+ * DM. The topic is created via `createForumTopic`, reused while the session
6
+ * remains active, and removed from the registry when the daemon deletes it on
7
+ * shutdown. The registry also tracks whether the one-time identity header has
8
+ * already been pinned, so it is sent exactly once per active topic, even across
9
9
  * reconnects.
10
10
  *
11
11
  * State is a plain serialisable map persisted beside the daemon state files;
@@ -50,11 +50,10 @@ export declare class TopicRegistry {
50
50
  /** The existing topic record for a session, if any. */
51
51
  get(sessionId: string): TopicRecord | undefined;
52
52
  /**
53
- * Return the existing topic for `sessionId`, or create one via `create`
54
- * (called only on first use). Reuse-on-resume: an existing record is
55
- * returned without invoking `create`.
53
+ * Return the existing active topic for `sessionId`, or create one via
54
+ * `create` (called only on first use).
56
55
  */
57
- getOrCreateTopic(sessionId: string, create: () => Promise<string>, now?: () => number): Promise<TopicRecord>;
56
+ getOrCreateTopic(sessionId: string, create: () => Promise<string>, now?: () => number, name?: string): Promise<TopicRecord>;
58
57
  /** Mark the identity header as sent for a session. Idempotent. */
59
58
  markIdentitySent(sessionId: string): void;
60
59
  /** Whether the identity header still needs sending for this session. */
@@ -64,6 +63,8 @@ export declare class TopicRegistry {
64
63
  * caller should `editForumTopic`), `false` when already current or unknown.
65
64
  */
66
65
  applyName(sessionId: string, name: string): boolean;
66
+ /** Remove a session topic record after Telegram deletes the topic. */
67
+ delete(sessionId: string): boolean;
67
68
  /** Serialise for atomic persistence beside the daemon state. */
68
69
  serialize(): TopicRegistryState;
69
70
  }
@@ -64,6 +64,13 @@ export interface MCPStdioServerConfig extends MCPServerConfigBase {
64
64
  command: string;
65
65
  args?: string[];
66
66
  env?: Record<string, string>;
67
+ /**
68
+ * When true, the child process is NOT given the host environment. Only a
69
+ * minimal OS allowlist (PATH/HOME/temp/locale) plus any explicit `env` keys
70
+ * are passed. Used for third-party plugin-bundle MCP servers so they cannot
71
+ * read host secrets from the inherited environment.
72
+ */
73
+ noInheritEnv?: boolean;
67
74
  cwd?: string;
68
75
  }
69
76
  /** HTTP server configuration (Streamable HTTP transport) */
@@ -10,6 +10,7 @@ import "./discovery";
10
10
  import type { CustomCommandsLoadResult } from "./extensibility/custom-commands";
11
11
  import type { CustomTool } from "./extensibility/custom-tools/types";
12
12
  import { type ExtensionFactory, type ExtensionUIContext, type LoadExtensionsResult, type ToolDefinition } from "./extensibility/extensions";
13
+ import { type ConstrainedPluginHook } from "./extensibility/gjc-plugins/constrained-hooks";
13
14
  import { type Skill, type SkillWarning } from "./extensibility/skills";
14
15
  import type { FileSlashCommand } from "./extensibility/slash-commands";
15
16
  import type { HindsightSessionState } from "./hindsight/state";
@@ -240,4 +241,5 @@ export interface BuildSystemPromptOptions {
240
241
  * as separate entries so providers can cache prompt prefixes without concatenating blocks.
241
242
  */
242
243
  export declare function buildSystemPrompt(options?: BuildSystemPromptOptions): Promise<BuildSystemPromptResult>;
244
+ export declare function createPluginHooksExtension(hooks: ConstrainedPluginHook[]): ExtensionFactory;
243
245
  export declare function createAgentSession(options?: CreateAgentSessionOptions): Promise<CreateAgentSessionResult>;
@@ -69,6 +69,7 @@ import type { WorkflowGateEmitter } from "../modes/shared/agent-wire/unattended-
69
69
  import type { PlanModeState } from "../plan-mode/state";
70
70
  import { type AgentRegistry } from "../registry/agent-registry";
71
71
  import { type DiscoverableMCPSearchIndex, type DiscoverableMCPTool } from "../runtime-mcp/discoverable-tool-metadata";
72
+ import { MCPManager } from "../runtime-mcp/manager";
72
73
  import { type SecretObfuscator } from "../secrets/obfuscator";
73
74
  import { type DiscoverableTool, type DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
74
75
  import type { AskAnswerSource, ToolSession } from "../tools";
@@ -77,7 +78,7 @@ import { type TodoItem, type TodoPhase } from "../tools/todo-write";
77
78
  import type { ClientBridge } from "./client-bridge";
78
79
  import { type ContributionPrepOptions, type ContributionPrepResult } from "./contribution-prep";
79
80
  import { type CustomMessage } from "./messages";
80
- import type { BranchSummaryEntry, NewSessionOptions, SessionContext, SessionManager } from "./session-manager";
81
+ import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionContext, SessionManager } from "./session-manager";
81
82
  import { ToolChoiceQueue } from "./tool-choice-queue";
82
83
  import { YieldQueue } from "./yield-queue";
83
84
  /** Session-specific events that extend the core AgentEvent */
@@ -233,6 +234,13 @@ export interface AgentSessionConfig {
233
234
  * **MUST NOT** dispose it on their own teardown.
234
235
  */
235
236
  ownedAsyncJobManager?: AsyncJobManager;
237
+ /**
238
+ * MCPManager whose lifecycle this session owns (top-level sessions that
239
+ * connected plugin-bundle MCP servers). Only the owned manager is
240
+ * disconnected on dispose; subagents and callers that merely observe the
241
+ * process-global manager **MUST NOT** dispose it on their own teardown.
242
+ */
243
+ ownedMcpManager?: MCPManager;
236
244
  /** Optional fork-context seed used to initialize a child session before its first prompt. */
237
245
  forkContextSeed?: ForkContextSeed;
238
246
  /** Optional provider state override. Fork-context children should omit this by default. */
@@ -676,6 +684,7 @@ export declare class AgentSession {
676
684
  get skillWarnings(): readonly SkillWarning[];
677
685
  getTodoPhases(): TodoPhase[];
678
686
  setTodoPhases(phases: TodoPhase[]): void;
687
+ applyCompactionPostAppendForTests(compactionEntryId: string, firstKeptEntryId: string, fromExtension?: boolean): Promise<CompactionEntry | undefined>;
679
688
  /**
680
689
  * Abort current operation and wait for agent to become idle.
681
690
  */
@@ -807,9 +816,10 @@ export declare class AgentSession {
807
816
  isFastForSubagentProvider(provider?: string): boolean;
808
817
  /**
809
818
  * True when the configured `serviceTier` resolves to `"priority"` for the
810
- * *currently selected model's provider*. Returns false for scoped tiers
811
- * that don't match (e.g. `"openai-only"` on an anthropic model) and when
812
- * no model is selected.
819
+ * *currently selected model's provider* AND fast mode was not auto-disabled
820
+ * for that provider this session. This is the current-model EFFECTIVE
821
+ * predicate (what the next request actually does); use {@link isFastForProvider}
822
+ * for pure configured intent (e.g. subagent/`modelRoles` display rows).
813
823
  */
814
824
  isFastModeActive(): boolean;
815
825
  setServiceTier(serviceTier: ServiceTier | undefined): void;
@@ -3,6 +3,14 @@ export interface BlobPutResult {
3
3
  path: string;
4
4
  get ref(): string;
5
5
  }
6
+ export interface CheckedBlobPutResult extends BlobPutResult {
7
+ bytes: number;
8
+ }
9
+ export declare class BlobCorruptError extends Error {
10
+ readonly hash: string;
11
+ readonly path: string;
12
+ constructor(hash: string, path: string);
13
+ }
6
14
  /**
7
15
  * Content-addressed blob store for externalizing large binary data (images) from session JSONL files.
8
16
  *
@@ -24,10 +32,22 @@ export declare class BlobStore {
24
32
  * Returns once the bytes are in the kernel page cache.
25
33
  */
26
34
  putSync(data: Buffer): BlobPutResult;
35
+ /**
36
+ * Durably install binary data as an immutable content-addressed blob.
37
+ *
38
+ * Callers that persist references to this blob must mutate canonical session entries only
39
+ * after this method returns successfully. A corrupt pre-existing target is reported with
40
+ * {@link BlobCorruptError}; it is never silently overwritten or trusted.
41
+ */
42
+ putImmutableSync(data: Buffer): CheckedBlobPutResult;
27
43
  /** Read blob by hash, returns Buffer or null if not found. */
28
44
  get(hash: string): Promise<Buffer | null>;
29
45
  /** Synchronously read blob by hash, returns Buffer or null if not found. */
30
46
  getSync(hash: string): Buffer | null;
47
+ /** Read blob by hash and verify its content hash; returns null if not found. */
48
+ getChecked(hash: string): Promise<Buffer | null>;
49
+ /** Synchronously read blob by hash and verify its content hash; returns null if not found. */
50
+ getCheckedSync(hash: string): Buffer | null;
31
51
  /** Check if a blob exists. */
32
52
  has(hash: string): Promise<boolean>;
33
53
  }
@@ -35,7 +55,9 @@ export declare class EphemeralBlobStore extends BlobStore {
35
55
  #private;
36
56
  constructor(dir: string);
37
57
  putSync(data: Buffer): BlobPutResult;
58
+ putImmutableSync(data: Buffer): CheckedBlobPutResult;
38
59
  getSync(hash: string): Buffer | null;
60
+ getCheckedSync(hash: string): Buffer | null;
39
61
  clear(): void;
40
62
  dispose(): void;
41
63
  }
@@ -44,8 +66,11 @@ export declare class MemoryBlobStore extends BlobStore {
44
66
  constructor();
45
67
  put(data: Buffer): Promise<BlobPutResult>;
46
68
  putSync(data: Buffer): BlobPutResult;
69
+ putImmutableSync(data: Buffer): CheckedBlobPutResult;
47
70
  get(hash: string): Promise<Buffer | null>;
48
71
  getSync(hash: string): Buffer | null;
72
+ getChecked(hash: string): Promise<Buffer | null>;
73
+ getCheckedSync(hash: string): Buffer | null;
49
74
  has(hash: string): Promise<boolean>;
50
75
  }
51
76
  export declare class ResidentBlobMissingError extends Error {
@@ -26,9 +26,51 @@ export interface SessionEntryBase {
26
26
  parentId: string | null;
27
27
  timestamp: string;
28
28
  }
29
+ export interface ColdSpillRef {
30
+ kind: "cold_spill";
31
+ ref: string;
32
+ encoding: "utf8" | "json";
33
+ originalChars: number;
34
+ sha256: string;
35
+ bytes: number;
36
+ }
37
+ export interface EvictedContentMarker {
38
+ evictedAt: number;
39
+ reason: "compacted_history";
40
+ compactionEntryId: string;
41
+ firstKeptEntryId: string;
42
+ payloads: Record<string, ColdSpillRef>;
43
+ }
44
+ export interface EvictCompactedContentResult {
45
+ evictedEntries: number;
46
+ hotCharsRemoved: number;
47
+ coldBlobBytes: number;
48
+ payloadRefs: number;
49
+ alreadyEvictedEntries: number;
50
+ coldSpillWriteCount: number;
51
+ coldSpillReadCount: number;
52
+ residentTextReadCount: number;
53
+ residentImageReadCount: number;
54
+ }
55
+ export interface SessionManagerObservabilityStats {
56
+ coldSpillWriteCount: number;
57
+ coldSpillReadCount: number;
58
+ residentTextReadCount: number;
59
+ residentImageReadCount: number;
60
+ publicMaterializerCallCount: number;
61
+ getEntryMaterializerCallCount: number;
62
+ getBranchMaterializerCallCount: number;
63
+ getEntriesMaterializerCallCount: number;
64
+ materializedEntriesCachePopulateCount: number;
65
+ pathOnlyContextBuildCount: number;
66
+ }
29
67
  export interface SessionMessageEntry extends SessionEntryBase {
30
68
  type: "message";
31
69
  message: AgentMessage;
70
+ /** Cold-spill marker: when present, heavy message content was moved to durable
71
+ * content-addressed blobs after compaction. The marker is entry-level session
72
+ * metadata (not a message field) so strict message types stay intact. */
73
+ evictedContent?: EvictedContentMarker;
32
74
  }
33
75
  export interface ThinkingLevelChangeEntry extends SessionEntryBase {
34
76
  type: "thinking_level_change";
@@ -148,6 +190,8 @@ export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
148
190
  display: boolean;
149
191
  /** Who initiated this message for billing/attribution semantics. */
150
192
  attribution?: MessageAttribution;
193
+ /** Cold-spill marker for custom-message content evicted after compaction. */
194
+ evictedContent?: EvictedContentMarker;
151
195
  }
152
196
  /** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */
153
197
  export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | ServiceTierChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | TtsrInjectionEntry | MCPToolSelectionEntry | SessionInitEntry | ModeChangeEntry;
@@ -480,6 +524,19 @@ export declare class SessionManager {
480
524
  * Returns undefined if no model change has been recorded.
481
525
  */
482
526
  getLastModelChangeRole(): string | undefined;
527
+ evictCompactedContent(firstKeptEntryId: string, compactionEntryId: string): EvictCompactedContentResult;
528
+ getObservabilityStatsForTests(): SessionManagerObservabilityStats;
529
+ hotRetainedMessageCharsForTests(): number;
530
+ getCanonicalEntryForTests(id: string): SessionEntry | undefined;
531
+ getEntryForFidelity(id: string): SessionEntry | undefined;
532
+ getBranchForFidelity(fromId?: string): SessionEntry[];
533
+ /**
534
+ * Walk the active branch without materializing resident blobs or rehydrating
535
+ * cold-spill payloads. Intended for metadata-only scans such as todo-phase
536
+ * sync; callers must not mutate returned entries.
537
+ */
538
+ getActivePathEntriesCanonical(fromId?: string): SessionEntry[];
539
+ getEntriesForExport(): SessionEntry[];
483
540
  getEntry(id: string): SessionEntry | undefined;
484
541
  /**
485
542
  * Get all direct children of an entry.
@@ -37,6 +37,12 @@ export interface FastStatusSessionLike {
37
37
  readonly model?: Model;
38
38
  /** Fast predicate against the main session tier (current model + `modelRoles`). */
39
39
  isFastForProvider(provider?: string): boolean;
40
+ /**
41
+ * Current-model EFFECTIVE fast state (intent minus any provider auto-disable).
42
+ * Used for the current-model row so it matches what the next request does.
43
+ * Optional so lightweight fakes can omit it; falls back to `isFastForProvider`.
44
+ */
45
+ isFastModeActive?(): boolean;
40
46
  /** Fast predicate against the effective subagent tier (`task.agentModelOverrides` roles). */
41
47
  isFastForSubagentProvider(provider?: string): boolean;
42
48
  resolveRoleModelWithThinking(role: string): {
@@ -47,6 +47,8 @@ export interface BuildSystemPromptOptions {
47
47
  toolNames?: string[];
48
48
  /** Text to append to system prompt. */
49
49
  appendSystemPrompt?: string;
50
+ /** Rendered GJC plugin system-appendix blocks (lower-authority, appended last). */
51
+ pluginAppendices?: string;
50
52
  /** Repeat full tool descriptions in system prompt. Default: false */
51
53
  repeatToolDescriptions?: boolean;
52
54
  /** Skills settings for discovery. */
@@ -4,6 +4,7 @@
4
4
  * Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
5
5
  */
6
6
  import type { AgentTelemetryConfig, ThinkingLevel } from "@gajae-code/agent-core";
7
+ import type { ServiceTier } from "@gajae-code/ai";
7
8
  import { ModelRegistry } from "../config/model-registry";
8
9
  import type { PromptTemplate } from "../config/prompt-templates";
9
10
  import { Settings } from "../config/settings";
@@ -58,6 +59,13 @@ export interface ExecutorOptions {
58
59
  authStorage?: AuthStorage;
59
60
  modelRegistry?: ModelRegistry;
60
61
  settings?: Settings;
62
+ /**
63
+ * Live service-tier intent of the parent session (`AgentSession.serviceTier`),
64
+ * used as the inherited tier when `task.serviceTier === "inherit"`. Passing the
65
+ * live value (not the stale settings snapshot) lets a runtime `/fast on` reach
66
+ * subagents, and a main-model fast-mode auto-disable does not clobber it.
67
+ */
68
+ inheritedServiceTier?: ServiceTier;
61
69
  /** Override local:// protocol options so subagent shares parent's local:// root */
62
70
  localProtocolOptions?: LocalProtocolOptions;
63
71
  /**
@@ -105,7 +113,7 @@ interface FinalizeSubprocessOutputResult {
105
113
  export declare const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yield with null data.";
106
114
  export declare const SUBAGENT_WARNING_MISSING_YIELD = "SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.";
107
115
  export declare function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult;
108
- export declare function createSubagentSettings(baseSettings: Settings): Settings;
116
+ export declare function createSubagentSettings(baseSettings: Settings, inheritedServiceTier?: ServiceTier): Settings;
109
117
  /**
110
118
  * Run a single agent in-process.
111
119
  */
@@ -1,5 +1,5 @@
1
1
  import type { AgentTelemetryConfig, AgentTool } from "@gajae-code/agent-core";
2
- import type { Model, ToolChoice } from "@gajae-code/ai";
2
+ import type { Model, ServiceTier, ToolChoice } from "@gajae-code/ai";
3
3
  import type { PromptTemplate } from "../config/prompt-templates";
4
4
  import type { Settings } from "../config/settings";
5
5
  import type { Skill } from "../extensibility/skills";
@@ -173,6 +173,8 @@ export interface ToolSession {
173
173
  agentOutputManager?: AgentOutputManager;
174
174
  /** Settings instance for passing to subagents */
175
175
  settings: Settings;
176
+ /** Live service-tier intent of the parent session, inherited by `inherit` subagents. */
177
+ serviceTier?: ServiceTier;
176
178
  /** Plan mode state (if active) */
177
179
  getPlanModeState?: () => PlanModeState | undefined;
178
180
  /** Goal mode state (if active or paused) */
@@ -25,6 +25,7 @@ export declare function parseChangelog(changelogPath: string): Promise<Changelog
25
25
  * binary-identity changelog).
26
26
  */
27
27
  export declare function getDisplayChangelogEntries(): ChangelogEntry[];
28
+ export declare function getInstalledVersionChangelogEntry(entries: readonly ChangelogEntry[], installedVersion: string): ChangelogEntry | undefined;
28
29
  /**
29
30
  * Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
30
31
  */