@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
@@ -12,12 +12,160 @@
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`.
19
19
  */
20
20
  import type { ExtensionFactory } from "../extensibility/extensions";
21
+ /** Where a `session_create` should run. Discriminated by `kind`. */
22
+ export type SessionCreateTarget = {
23
+ kind: "existing_path";
24
+ path: string;
25
+ } | {
26
+ kind: "worktree";
27
+ repo: string;
28
+ branch: string;
29
+ } | {
30
+ kind: "plain_dir";
31
+ path: string;
32
+ };
33
+ /** Identifies the session a `session_close` targets. */
34
+ export interface SessionCloseTarget {
35
+ sessionId: string;
36
+ /** Expected GJC-managed tmux session name (defense-in-depth match). */
37
+ tmuxSession?: string;
38
+ /** Expected `@gjc-session-state-file` tag (defense-in-depth match). */
39
+ sessionStateFile?: string;
40
+ }
41
+ /** Identifies the session a `session_resume` targets. */
42
+ export interface SessionResumeTarget {
43
+ sessionIdOrPrefix: string;
44
+ /** Optional repo/working-dir hint to disambiguate matches. */
45
+ path?: string;
46
+ }
47
+ /** Create a new session. */
48
+ export interface SessionCreateFrame {
49
+ type: "session_create";
50
+ requestId: string;
51
+ /** Deterministic lifecycle marker preallocated by the daemon before spawn. */
52
+ lifecycleRequestId: string;
53
+ /** Session id the daemon preallocated and propagates to the child. */
54
+ intendedSessionId: string;
55
+ /** Telegram update id (idempotency key on the daemon side). */
56
+ updateId: number;
57
+ chatId: string;
58
+ /** Control-endpoint token authorizing this frame. */
59
+ token: string;
60
+ target: SessionCreateTarget;
61
+ /** Reference to the daemon-written, once-consumed startup-prompt file. */
62
+ startupPromptRef?: string;
63
+ }
64
+ /** Close (hard-kill, history preserved) a session. */
65
+ export interface SessionCloseFrame {
66
+ type: "session_close";
67
+ requestId: string;
68
+ updateId: number;
69
+ chatId: string;
70
+ token: string;
71
+ target: SessionCloseTarget;
72
+ /** Hard-kill even if a live pane is attached (GJC-managed only). */
73
+ force?: boolean;
74
+ }
75
+ /** Resume a session (reattach if alive, else cold-restart from history). */
76
+ export interface SessionResumeFrame {
77
+ type: "session_resume";
78
+ requestId: string;
79
+ updateId: number;
80
+ chatId: string;
81
+ token: string;
82
+ target: SessionResumeTarget;
83
+ startupPromptRef?: string;
84
+ }
85
+ /** Any client -> ingress lifecycle request frame. */
86
+ export type SessionLifecycleRequest = SessionCreateFrame | SessionCloseFrame | SessionResumeFrame;
87
+ /** Terminal status of a lifecycle request. */
88
+ export type LifecycleStatus = "ok" | "error";
89
+ /** A connected session's per-session endpoint, returned to the control client. */
90
+ export interface LifecycleEndpoint {
91
+ url: string;
92
+ token: string;
93
+ }
94
+ /** The Telegram topic/thread a session is surfaced in. */
95
+ export interface LifecycleTopic {
96
+ chatId: string;
97
+ threadId: string;
98
+ }
99
+ /** How a create request was correlated to its spawned session. */
100
+ export type MatchedBy = "spawn_marker" | "session_ready";
101
+ /** Response to a successful `session_create`. */
102
+ export interface SessionCreateResponseFrame {
103
+ type: "session_create_response";
104
+ requestId: string;
105
+ status: LifecycleStatus;
106
+ lifecycleRequestId: string;
107
+ sessionId: string;
108
+ matchedBy: MatchedBy;
109
+ endpoint: LifecycleEndpoint;
110
+ topic: LifecycleTopic;
111
+ target: SessionCreateTarget;
112
+ }
113
+ /** Response to a successful `session_close`. */
114
+ export interface SessionCloseResponseFrame {
115
+ type: "session_close_response";
116
+ requestId: string;
117
+ status: LifecycleStatus;
118
+ sessionId: string;
119
+ processGone: boolean;
120
+ historyPreserved: boolean;
121
+ endpointStale: boolean;
122
+ }
123
+ /** Whether a resume reattached to a live session or cold-restarted a dead one. */
124
+ export type ResumeMode = "reattached" | "cold_restarted";
125
+ /** Response to a successful `session_resume`. */
126
+ export interface SessionResumeResponseFrame {
127
+ type: "session_resume_response";
128
+ requestId: string;
129
+ status: LifecycleStatus;
130
+ sessionId: string;
131
+ mode: ResumeMode;
132
+ endpoint: LifecycleEndpoint;
133
+ topic: LifecycleTopic;
134
+ }
135
+ /** Machine-readable reason a lifecycle request failed. */
136
+ export type LifecycleErrorReason = "unauthorized" | "rate_limited" | "duplicate_conflict" | "invalid_target" | "ambiguous_target" | "spawn_failed" | "discovery_timeout" | "readiness_timeout" | "close_refused" | "not_found" | "terminal_uncertain";
137
+ /** A candidate returned with an `ambiguous_target` resume error. */
138
+ export interface ResumeCandidate {
139
+ sessionId: string;
140
+ path?: string;
141
+ /** Last-activity epoch-millis (session history file mtime), if known. */
142
+ mtimeMs?: number;
143
+ }
144
+ /** A structured lifecycle error frame. */
145
+ export interface SessionLifecycleErrorFrame {
146
+ type: "session_lifecycle_error";
147
+ requestId: string;
148
+ status: LifecycleStatus;
149
+ reason: LifecycleErrorReason;
150
+ message: string;
151
+ candidates?: ResumeCandidate[];
152
+ }
153
+ /** Any ingress -> client lifecycle response frame. */
154
+ export type SessionLifecycleResponse = SessionCreateResponseFrame | SessionCloseResponseFrame | SessionResumeResponseFrame | SessionLifecycleErrorFrame;
155
+ /**
156
+ * Replayable per-session readiness signal (mirror of the Rust `session_ready`
157
+ * frame). Buffered and replayed to late clients so WS-open alone never implies
158
+ * the session is live and surfaced.
159
+ */
160
+ export interface SessionReadyFrame {
161
+ type: "session_ready";
162
+ sessionId: string;
163
+ lifecycleRequestId?: string;
164
+ startupPromptRef?: string;
165
+ repo?: string;
166
+ branch?: string;
167
+ title?: string;
168
+ }
21
169
  /**
22
170
  * Best-effort real repository name (no git spawn): resolves the main worktree
23
171
  * root directory so linked worktrees report the repo (e.g. `gajae-code`)
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Paired-chat /session_* command grammar (G009).
3
+ *
4
+ * Pure parser + shared target validator for the Telegram session-lifecycle
5
+ * commands. The daemon parses an inbound paired-chat message here, then attaches
6
+ * transport identity (chatId/updateId/token/requestId) and routes the resulting
7
+ * frame to the orchestrator. Keeping this pure makes the grammar, the MVP
8
+ * prompt-rejection, and target validation unit-testable without the daemon.
9
+ *
10
+ * MVP scope: an initial prompt (`-- <prompt>`) is REJECTED with usage text — no
11
+ * prompt text ever enters a frame, audit, log, or response until daemon-owned
12
+ * 0600 prompt refs are designed.
13
+ */
14
+ import type { SessionCloseTarget, SessionCreateTarget, SessionLifecycleResponse, SessionResumeTarget } from "./index";
15
+ export type LifecycleCommandVerb = "session_create" | "session_close" | "session_resume";
16
+ /** A parsed, validated lifecycle command (transport identity added by caller). */
17
+ export type ParsedLifecycleCommand = {
18
+ kind: "create";
19
+ target: SessionCreateTarget;
20
+ } | {
21
+ kind: "close";
22
+ target: SessionCloseTarget;
23
+ } | {
24
+ kind: "resume";
25
+ target: SessionResumeTarget;
26
+ } | {
27
+ kind: "recent";
28
+ which: "create" | "resume" | "all";
29
+ } | {
30
+ kind: "usage";
31
+ message: string;
32
+ } | {
33
+ kind: "reject";
34
+ reason: "invalid_target" | "prompt_unsupported";
35
+ message: string;
36
+ } | {
37
+ kind: "none";
38
+ };
39
+ /** True when the text begins a /session_* command (cheap pre-gate). */
40
+ export declare function isLifecycleCommandText(text: string | undefined): boolean;
41
+ /**
42
+ * Parse a paired-chat message into a lifecycle command. Returns `none` for
43
+ * non-lifecycle text, `usage`/`reject` for malformed input (no side effect), or
44
+ * a validated `create`/`close`/`resume`/`recent` intent.
45
+ *
46
+ * The caller MUST have already enforced paired-chat authorization; this function
47
+ * performs grammar + target validation only.
48
+ */
49
+ export declare function parseLifecycleCommand(text: string | undefined): ParsedLifecycleCommand;
50
+ /** The canonical usage text (exported for the daemon's help replies). */
51
+ export declare function lifecycleUsage(): string;
52
+ /**
53
+ * Shared target validator reused at the policy/effect boundary (after paired-chat
54
+ * auth, before any side effect). Returns null when valid, or an `invalid_target`
55
+ * reason. The orchestrator remains authoritative; this is a defensive pre-check
56
+ * the parser and any other entry point share.
57
+ */
58
+ export declare function validateLifecycleTarget(verb: LifecycleCommandVerb, target: SessionCreateTarget | SessionCloseTarget | SessionResumeTarget): {
59
+ ok: true;
60
+ } | {
61
+ ok: false;
62
+ reason: "invalid_target";
63
+ message: string;
64
+ };
65
+ /**
66
+ * Map a lifecycle response/error to a user-facing Telegram message (G010).
67
+ *
68
+ * Only derives text from sessionId, mode, reason, a safe message, and candidate
69
+ * {sessionId,path} — never a token or prompt. Each error reason gets tailored,
70
+ * actionable copy; an "in progress" pending response is surfaced distinctly.
71
+ */
72
+ export declare function formatLifecycleOutcome(r: SessionLifecycleResponse): string;
@@ -0,0 +1,98 @@
1
+ import type { ResumeCandidate, SessionCreateFrame, SessionLifecycleRequest, SessionLifecycleResponse } from "./index";
2
+ import { type AuditEvent, type CreateEffectResult, type LedgerStore, type LifecycleOutcome, type OrchestratorDeps, type ResumeEffectResult } from "./lifecycle-orchestrator";
3
+ /** Minimal view of the native control server this runtime depends on. */
4
+ export interface ControlServerLike {
5
+ onLifecycleRequest(cb: (err: Error | null, req: {
6
+ kind: string;
7
+ requestId: string;
8
+ payloadJson: string;
9
+ }) => void): void;
10
+ respond(responseJson: string): void;
11
+ }
12
+ /**
13
+ * A startable control server (the native NotificationControlServer, or a fake in
14
+ * tests). Extends {@link ControlServerLike} with the start/stop lifecycle the
15
+ * daemon owns.
16
+ */
17
+ export interface LifecycleControlServer extends ControlServerLike {
18
+ start(): Promise<unknown>;
19
+ stop(): void;
20
+ }
21
+ /** Factory the daemon uses to construct a control server bound to its ownership. */
22
+ export type LifecycleControlServerFactory = (input: {
23
+ token: string;
24
+ ownerId: string;
25
+ agentDir: string;
26
+ }) => LifecycleControlServer;
27
+ /** Atomic + fsynced file-backed idempotency ledger store. */
28
+ export declare function fileLedgerStore(idempotencyFile: string): LedgerStore;
29
+ /** Append-only JSONL audit sink (0600). Never receives tokens or raw prompts. */
30
+ export declare function fileAudit(auditPath: string): (e: AuditEvent) => void;
31
+ /** Simple per-chat sliding-window create rate limiter. */
32
+ export declare function createRateLimiter(maxPerWindow: number, windowMs: number): (chatId: string, nowMs: number) => boolean;
33
+ /** Build the `gjc` argv for a create target (existing path / worktree / dir).
34
+ *
35
+ * The launched session id is carried via `GJC_SESSION_ID` in the child env (see
36
+ * {@link daemonSpawnCreate}); the root `gjc` launcher has no `--session-id`
37
+ * flag, so it must never appear in argv. Only flags the launch parser actually
38
+ * supports are emitted (`--worktree <branch>` for worktree targets). */
39
+ export declare function buildCreateArgv(frame: SessionCreateFrame, _ids: {
40
+ intendedSessionId: string;
41
+ startupPromptRef?: string;
42
+ }): {
43
+ cwd: string;
44
+ args: string[];
45
+ };
46
+ /** Real daemon-safe tmux launcher: detached `tmux new-session -d` + GJC tags. */
47
+ export declare function daemonSpawnCreate(env?: NodeJS.ProcessEnv): (frame: SessionCreateFrame, ids: {
48
+ lifecycleRequestId: string;
49
+ intendedSessionId: string;
50
+ startupPromptRef?: string;
51
+ }) => Promise<CreateEffectResult>;
52
+ /** Real force-close effect (GJC-managed only, id-matched). */
53
+ export declare function daemonCloseSession(env?: NodeJS.ProcessEnv): (target: {
54
+ sessionId: string;
55
+ tmuxSession?: string;
56
+ sessionStateFile?: string;
57
+ }) => Promise<{
58
+ processGone: boolean;
59
+ }>;
60
+ /** Real resume effect: reattach if a live GJC session matches; else resolve the
61
+ * prefix against saved history and fail closed (`ambiguous`/`notFound`) before
62
+ * cold-restarting exactly one resolved session via the daemon-safe launcher. */
63
+ export declare function daemonResumeSession(env?: NodeJS.ProcessEnv, opts?: {
64
+ sessionsRoot?: string;
65
+ }): (target: {
66
+ sessionIdOrPrefix: string;
67
+ path?: string;
68
+ }) => Promise<ResumeEffectResult | {
69
+ ambiguous: ResumeCandidate[];
70
+ } | {
71
+ notFound: true;
72
+ }>;
73
+ /** Translate an orchestrator outcome into a wire response frame. */
74
+ export declare function outcomeToResponse(frame: SessionLifecycleRequest, outcome: LifecycleOutcome): SessionLifecycleResponse;
75
+ /**
76
+ * Wire a control server's lifecycle requests through the orchestrator.
77
+ *
78
+ * Handlers run on a single serial queue (a promise chain): the daemon owns the
79
+ * one control endpoint, so serializing here makes each request's ledger
80
+ * read -> classify -> write atomic with respect to every other request. Two
81
+ * identical updates that arrive nearly simultaneously can no longer both
82
+ * classify as `new` and both spawn — the second sees the first's persisted
83
+ * `in_progress`/`success` entry and re-acks instead.
84
+ */
85
+ export declare function attachLifecycleControl(server: ControlServerLike, deps: OrchestratorDeps): void;
86
+ /** Assemble real orchestrator deps for the daemon (ledger/audit under agentDir). */
87
+ export declare function buildOrchestratorDeps(input: {
88
+ pairedChatId: string;
89
+ agentNotificationsDir: string;
90
+ /** Root of saved session histories (`<agentDir>/sessions`), for resume resolution. */
91
+ sessionsRoot?: string;
92
+ env?: NodeJS.ProcessEnv;
93
+ }): OrchestratorDeps;
94
+ /**
95
+ * Default production factory: a real native NotificationControlServer bound to
96
+ * the daemon's control token, owner id, and agent dir.
97
+ */
98
+ export declare const createNativeControlServer: LifecycleControlServerFactory;
@@ -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>;
@@ -0,0 +1,52 @@
1
+ export interface NotificationOperatorTimerDeps {
2
+ now?: () => number;
3
+ setTimeoutImpl?: typeof setTimeout;
4
+ clearTimeoutImpl?: typeof clearTimeout;
5
+ setIntervalImpl?: typeof setInterval;
6
+ clearIntervalImpl?: typeof clearInterval;
7
+ }
8
+ export interface NotificationOperatorRuntimeState {
9
+ running: boolean;
10
+ stopRequested: boolean;
11
+ activeAbort: boolean;
12
+ }
13
+ export interface OperatorBackoffOptions {
14
+ initialMs: number;
15
+ maxMs: number;
16
+ factor?: number;
17
+ }
18
+ export declare class OperatorBackoffPolicy {
19
+ #private;
20
+ constructor(opts: OperatorBackoffOptions);
21
+ next(): number;
22
+ reset(): void;
23
+ get currentMs(): number;
24
+ }
25
+ export interface OperatorRoute<TContext> {
26
+ name: string;
27
+ matches(event: Record<string, unknown>): boolean;
28
+ handle(context: TContext, event: Record<string, unknown>): Promise<void> | void;
29
+ }
30
+ export declare class OperatorEventRouter<TContext> {
31
+ readonly routes: OperatorRoute<TContext>[];
32
+ add(input: OperatorRoute<TContext>): this;
33
+ dispatch(context: TContext, event: Record<string, unknown>): Promise<boolean>;
34
+ }
35
+ export declare class NotificationOperatorRuntime {
36
+ #private;
37
+ constructor(deps?: NotificationOperatorTimerDeps);
38
+ get state(): NotificationOperatorRuntimeState;
39
+ start(): void;
40
+ stop(): void;
41
+ requestStop(): void;
42
+ get running(): boolean;
43
+ get stopRequested(): boolean;
44
+ createAbortController(): AbortController;
45
+ clearAbortController(controller: AbortController): void;
46
+ startInterval(name: string, intervalMs: number, tick: () => void): void;
47
+ stopInterval(name: string): void;
48
+ stopAllIntervals(): void;
49
+ runExclusive(name: string, fn: () => Promise<void>): Promise<void>;
50
+ sleep(ms: number, signal?: AbortSignal): Promise<void>;
51
+ now(): number;
52
+ }
@@ -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[];