@gajae-code/coding-agent 0.12.11 → 0.12.13

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 (140) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/types/cli/marketplace-hint.d.ts +28 -0
  3. package/dist/types/cli/notify-cli.d.ts +42 -1
  4. package/dist/types/cli/update-cli.d.ts +9 -3
  5. package/dist/types/cli.d.ts +10 -0
  6. package/dist/types/commands/notify.d.ts +6 -0
  7. package/dist/types/config/file-lock-gc.d.ts +13 -1
  8. package/dist/types/config/file-lock.d.ts +3 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/models-config-schema.d.ts +99 -0
  11. package/dist/types/coordinator/contract.d.ts +1 -1
  12. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  13. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  14. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  15. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  16. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  17. package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
  18. package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
  19. package/dist/types/main.d.ts +6 -2
  20. package/dist/types/modes/acp/acp-agent.d.ts +24 -1
  21. package/dist/types/modes/components/session-selector.d.ts +1 -1
  22. package/dist/types/modes/utils/host-status.d.ts +23 -0
  23. package/dist/types/runtime-mcp/types.d.ts +2 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  25. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  26. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  28. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  29. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  30. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  31. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  32. package/dist/types/sdk/bus/html-format.d.ts +10 -0
  33. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  34. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  35. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  36. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  37. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  38. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +28 -7
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -17
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +20 -1
  42. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  43. package/dist/types/sdk/host/host.d.ts +52 -0
  44. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  45. package/dist/types/sdk/session-activation.d.ts +41 -0
  46. package/dist/types/sdk/session.d.ts +18 -0
  47. package/dist/types/sdk/startup-capability.d.ts +8 -1
  48. package/dist/types/session/agent-session.d.ts +7 -2
  49. package/dist/types/session/artifacts.d.ts +2 -2
  50. package/dist/types/session/session-manager.d.ts +40 -6
  51. package/dist/types/tools/index.d.ts +15 -0
  52. package/dist/types/utils/npm-registry.d.ts +71 -0
  53. package/examples/extensions/README.md +38 -4
  54. package/examples/extensions/ooo-bridge.ts +15 -0
  55. package/package.json +8 -8
  56. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  57. package/src/async/job-manager.ts +9 -2
  58. package/src/cli/fast-help.ts +1 -0
  59. package/src/cli/marketplace-hint.ts +39 -0
  60. package/src/cli/notify-cli.ts +226 -2
  61. package/src/cli/plugin-cli.ts +10 -0
  62. package/src/cli/update-cli.ts +90 -23
  63. package/src/cli.ts +23 -0
  64. package/src/commands/notify.ts +27 -3
  65. package/src/commands/sdk.ts +1 -0
  66. package/src/config/file-lock-gc.ts +65 -26
  67. package/src/config/file-lock.ts +61 -16
  68. package/src/config/model-profiles.ts +46 -30
  69. package/src/config/model-registry.ts +17 -5
  70. package/src/config/models-config-schema.ts +9 -4
  71. package/src/coordinator/contract.ts +1 -0
  72. package/src/coordinator-mcp/question-state.ts +2 -0
  73. package/src/coordinator-mcp/server.ts +358 -7
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  76. package/src/extensibility/extensions/index.ts +1 -0
  77. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  78. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  79. package/src/extensibility/extensions/runner.ts +15 -2
  80. package/src/extensibility/extensions/types.ts +2 -0
  81. package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
  82. package/src/gjc-runtime/deep-interview-state.ts +27 -3
  83. package/src/gjc-runtime/gc-render.ts +8 -1
  84. package/src/gjc-runtime/gc-runtime.ts +15 -1
  85. package/src/gjc-runtime/launch-tmux.ts +53 -1
  86. package/src/internal-urls/docs-index.generated.ts +14 -13
  87. package/src/main.ts +214 -70
  88. package/src/modes/DESIGN.md +8 -5
  89. package/src/modes/acp/acp-agent.ts +123 -7
  90. package/src/modes/components/footer.ts +10 -18
  91. package/src/modes/components/model-selector.ts +52 -5
  92. package/src/modes/components/session-selector.ts +26 -3
  93. package/src/modes/components/status-line/gh.ts +23 -2
  94. package/src/modes/controllers/event-controller.ts +3 -0
  95. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  96. package/src/modes/controllers/input-controller.ts +11 -0
  97. package/src/modes/controllers/selector-controller.ts +8 -2
  98. package/src/modes/interactive-mode.ts +7 -1
  99. package/src/modes/print-mode.ts +10 -0
  100. package/src/modes/utils/host-status.ts +38 -0
  101. package/src/prompts/system/project-prompt.md +19 -0
  102. package/src/runtime-mcp/types.ts +2 -0
  103. package/src/sdk/broker/lifecycle.ts +60 -3
  104. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  105. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  106. package/src/sdk/bus/chat-daemon-control.ts +16 -3
  107. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  108. package/src/sdk/bus/control-plane-frames.ts +51 -0
  109. package/src/sdk/bus/conversation-store.ts +29 -1
  110. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  111. package/src/sdk/bus/html-format.ts +19 -3
  112. package/src/sdk/bus/index.ts +91 -12
  113. package/src/sdk/bus/notification-service.ts +28 -1
  114. package/src/sdk/bus/rich-render.ts +3 -2
  115. package/src/sdk/bus/slack-daemon.ts +99 -4
  116. package/src/sdk/bus/slack-live-provider.ts +32 -2
  117. package/src/sdk/bus/slack-provider.ts +11 -0
  118. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  119. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  120. package/src/sdk/bus/telegram-daemon-contract.ts +28 -7
  121. package/src/sdk/bus/telegram-daemon.ts +2007 -509
  122. package/src/sdk/bus/telegram-reference.ts +114 -15
  123. package/src/sdk/bus/topic-registry.ts +674 -181
  124. package/src/sdk/host/host.ts +180 -1
  125. package/src/sdk/lifecycle-session.ts +8 -2
  126. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  127. package/src/sdk/session-activation.ts +127 -0
  128. package/src/sdk/session.ts +144 -29
  129. package/src/sdk/startup-capability.ts +10 -1
  130. package/src/session/agent-session.ts +220 -13
  131. package/src/session/artifacts.ts +56 -13
  132. package/src/session/internal/managed-session-storage.ts +228 -7
  133. package/src/session/session-manager.ts +225 -60
  134. package/src/setup/provider-presets.json +3 -3
  135. package/src/task/index.ts +92 -44
  136. package/src/thinking-metadata.ts +2 -1
  137. package/src/tools/ask.ts +10 -0
  138. package/src/tools/index.ts +15 -0
  139. package/src/tools/todo-write.ts +4 -4
  140. package/src/utils/npm-registry.ts +660 -0
@@ -16,7 +16,7 @@ import type { PrintModeOptions } from "./modes/print-mode";
16
16
  import { initTheme } from "./modes/theme/theme";
17
17
  import type { SubmittedUserInput } from "./modes/types";
18
18
  import type { MCPManager } from "./runtime-mcp";
19
- import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage } from "./sdk";
19
+ import { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession, discoverAuthStorage } from "./sdk";
20
20
  import type { AgentSession } from "./session/agent-session";
21
21
  import { type ResumeSessionIdentity, type SessionDestination, type SessionDirectoryMigrationPolicy, type SessionInfo, SessionManager, type StrictSessionOpenResult } from "./session/session-manager";
22
22
  import { runStartupCredentialAutoImportIfNeeded } from "./setup/credential-auto-import";
@@ -55,6 +55,7 @@ type StartupModelProfileArgs = {
55
55
  parsedArgs: Pick<Args, "default" | "model" | "mpreset" | "thinking">;
56
56
  startupModel?: CreateAgentSessionOptions["model"];
57
57
  startupThinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
58
+ preferCachedModels?: boolean;
58
59
  };
59
60
  export declare function applyStartupModelProfiles(args: StartupModelProfileArgs): Promise<void>;
60
61
  export declare function applyStartupModelProfilesOrExit(args: StartupModelProfileArgs): Promise<void>;
@@ -74,6 +75,9 @@ export declare function applyStartupModelProfilesForRoot(args: StartupModelProfi
74
75
  }): Promise<{
75
76
  recoverableErrors: string[];
76
77
  }>;
78
+ type DeferredModelProfileStartup = () => Promise<{
79
+ recoverableErrors: string[];
80
+ }>;
77
81
  interface InteractiveModeFactoryOptions {
78
82
  session: AgentSession;
79
83
  version: string;
@@ -95,7 +99,7 @@ export declare const BARE_RESUME_INTERACTIVE_ERROR = "--resume requires an inter
95
99
  /** Resolves only the managed agent root needed for pre-consent picker inventory. */
96
100
  export declare function resolveManagedAgentDirForScope(_cwd: string): string;
97
101
  export declare const BARE_RESUME_OPEN_ERROR = "Could not open the selected session. Use --resume <id>.";
98
- export declare function runInteractiveMode(session: AgentSession, version: string, changelogMarkdown: string | undefined, notifs: (InteractiveModeNotify | null)[], startupUpdate: StartupUpdateOrchestrator, initialMessages: string[], setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void, lspServers: LspStartupServerInfo[] | undefined, mcpManager: MCPManager | undefined, eventBus?: EventBus, initialMessage?: string, initialImages?: ImageContent[], createInteractiveMode?: CreateInteractiveMode, resumeAction?: "continue-tail" | "open-idle"): Promise<void>;
102
+ export declare function runInteractiveMode(session: AgentSession, version: string, changelogMarkdown: string | undefined, notifs: (InteractiveModeNotify | null)[], startupUpdate: StartupUpdateOrchestrator, initialMessages: string[], setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void, lspServers: LspStartupServerInfo[] | undefined, mcpManager: MCPManager | undefined, eventBus?: EventBus, initialMessage?: string, initialImages?: ImageContent[], createInteractiveMode?: CreateInteractiveMode, resumeAction?: "continue-tail" | "open-idle", startDeferredMcpConfig?: CreateAgentSessionResult["startDeferredMcpConfig"], startDeferredModelProfiles?: DeferredModelProfileStartup): Promise<void>;
99
103
  export declare function getChangelogForDisplay(parsed: Args): Promise<string | undefined>;
100
104
  export declare function createSessionManager(parsed: Args, cwd: string, activeSettings?: Settings): Promise<SessionManager | undefined>;
101
105
  /**
@@ -32,11 +32,32 @@ export interface TranscriptReplayContent {
32
32
  };
33
33
  }
34
34
  export declare function transcriptReplayContent(entry: unknown): TranscriptReplayContent;
35
+ /**
36
+ * Collect every page of `providers.list/active` (Q29, GJC >= 0.12.8) and
37
+ * return the providers with usable stored credentials or credentialless
38
+ * connection kinds, mirroring the TUI model picker's
39
+ * `modelRegistry.getAvailable()`. The openwebui-gjc-adapter applies the same
40
+ * filter to `/v1/models`. Q29 pages are byte-bounded and can span multiple
41
+ * pages when custom provider ids inflate the payload, so all pages are
42
+ * consumed before the provider set is built. Returns undefined only when the
43
+ * session host rejects the query with `operation_not_session_owned`; any
44
+ * other query failure or malformed page is thrown.
45
+ */
46
+ export declare function collectActiveProviderIds(adapter: Pick<AcpSdkAdapter, "query">): Promise<ReadonlySet<string> | undefined>;
47
+ /**
48
+ * Collect every page of `models.list/current` (Q10) into one catalog so the
49
+ * active-provider filter never drops models that only appear on later pages.
50
+ * The SDK pages Q10 at a fixed byte target (256 KiB), which a fully
51
+ * configured catalog can exceed. Returns the same
52
+ * `{ result: { page: { items } } }` envelope `pageItems` consumes.
53
+ */
54
+ export declare function collectModelCatalog(adapter: Pick<AcpSdkAdapter, "query">): Promise<unknown>;
35
55
  /** Maps live canonical SDK config and the selected model catalog into the ACP 1.2.1 session state surface. */
36
- export declare function acpSessionStateFromConfig(query: unknown, modelCatalogQuery?: unknown, modelPreset?: string): {
56
+ export declare function acpSessionStateFromConfig(query: unknown, modelCatalogQuery?: unknown, modelPreset?: string, activeProviders?: ReadonlySet<string>): {
37
57
  configOptions: ({
38
58
  id: string;
39
59
  name: string;
60
+ category: "mode";
40
61
  type: "select";
41
62
  currentValue: string;
42
63
  options: {
@@ -46,6 +67,7 @@ export declare function acpSessionStateFromConfig(query: unknown, modelCatalogQu
46
67
  } | {
47
68
  id: "model";
48
69
  name: string;
70
+ category: "model";
49
71
  type: "select";
50
72
  currentValue: string;
51
73
  options: {
@@ -73,6 +95,7 @@ export declare function acpSessionStateFromConfig(query: unknown, modelCatalogQu
73
95
  } | {
74
96
  id: "thinking";
75
97
  name: string;
98
+ category: "thought_level";
76
99
  type: "select";
77
100
  currentValue: string;
78
101
  options: {
@@ -35,7 +35,7 @@ export declare class SessionSelectorComponent extends Container {
35
35
  private readonly onDelete?;
36
36
  private readonly inspector?;
37
37
  private readonly onSelection?;
38
- constructor(sessions: SessionInfo[], onSelect: (sessionPath: string) => void, onCancel: () => void, onExit: () => void, onDelete?: ((session: SessionInfo) => Promise<boolean>) | undefined, inspector?: SessionInspector | undefined, onSelection?: ((selection: SessionSelectionResult) => void) | undefined);
38
+ constructor(sessions: SessionInfo[], onSelect: (sessionPath: string) => void | Promise<void>, onCancel: () => void, onExit: () => void, onDelete?: ((session: SessionInfo) => Promise<boolean>) | undefined, inspector?: SessionInspector | undefined, onSelection?: ((selection: SessionSelectionResult) => void) | undefined);
39
39
  setOnRequestRender(callback: () => void): void;
40
40
  handleInput(keyData: string): void;
41
41
  getSessionList(): SessionList;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Host-terminal agent status markers.
3
+ *
4
+ * Interactive turns announce their state as an OSC 777 sequence:
5
+ *
6
+ * ESC ] 777 ; notify;Terax;gjc;<event> BEL
7
+ *
8
+ * A terminal that understands the sequence (Terax reads it to drive its per-tab
9
+ * agent status and header bell) reacts; every other terminal discards the OSC
10
+ * as unknown, exactly like the OSC 133 prompt marks and OSC 7 cwd reports that
11
+ * shells emit unconditionally. So there is no host sniffing and no env gate:
12
+ * any host that wants agent status can parse the stream without gjc knowing it
13
+ * exists.
14
+ *
15
+ * `notify;Terax;` is the wire identifier of the only protocol that currently
16
+ * carries per-turn agent state, not a host check - the payload names gjc as the
17
+ * reporting agent.
18
+ *
19
+ * Interactive TUI only. Print and RPC mode stdout stays byte-for-byte parseable
20
+ * because they never reach these controllers.
21
+ */
22
+ export type HostStatusEvent = "working" | "attention" | "finished";
23
+ export declare function emitHostStatus(event: HostStatusEvent, output?: Pick<NodeJS.WriteStream, "write">): void;
@@ -186,6 +186,8 @@ export type MCPContent = MCPTextContent | MCPImageContent | MCPResourceContent;
186
186
  export interface MCPToolCallResult {
187
187
  content: MCPContent[];
188
188
  isError?: boolean;
189
+ /** MCP result metadata, encoded as `_meta` on the wire. */
190
+ _meta?: Record<string, unknown>;
189
191
  }
190
192
  /** Expected configured-server transport or protocol failure. */
191
193
  export declare class MCPExpectedFailure extends Error {
@@ -48,6 +48,16 @@ export interface SessionLifecycleTranscriptIdentity {
48
48
  mtimeNs: string;
49
49
  sha256: string;
50
50
  }
51
+ /**
52
+ * When a lifecycle-managed session publishes its replayable readiness signal.
53
+ *
54
+ * `immediate` is the stock contract. `deferred` prepares the session instead:
55
+ * the child publishes a distinct prepared signal, keeps `session_ready`
56
+ * withheld, and stays unusable for input until it is explicitly activated. It
57
+ * is broker-issued and session-scoped precisely so a prepared session can never
58
+ * be produced by an inherited process-global flag.
59
+ */
60
+ export type SessionLifecycleReadiness = "immediate" | "deferred";
51
61
  export interface SessionLifecycleLaunchRequest {
52
62
  operation: "session.create" | "session.fork" | "session.resume";
53
63
  sessionId: string;
@@ -64,6 +74,8 @@ export interface SessionLifecycleLaunchRequest {
64
74
  modelPreset?: string;
65
75
  mcpServers?: SessionLifecycleMcpServer[];
66
76
  worktree?: SessionLifecycleWorktreeTarget;
77
+ /** Absent means the stock immediate contract; `deferred` prepares the session. */
78
+ readiness?: SessionLifecycleReadiness;
67
79
  receivedAt: number;
68
80
  requestedReadinessTimeoutMs: number;
69
81
  semanticReadyDeadlineAt: number;
@@ -1,9 +1,12 @@
1
+ import { type ChatDaemonCommandBindInput, type ChatDaemonCommandOutcome } from "./chat-daemon-command-channel";
1
2
  import { type ChatDaemonKind, renewChatDaemonHeartbeat } from "./chat-daemon-control";
2
3
  import { type ChatDaemonRuntimeConfig } from "./chat-daemon-runtime";
3
4
  export interface ChatDaemonRuntimeHandle {
4
5
  start(): Promise<void>;
5
6
  stop(): Promise<void>;
6
7
  transportHealthy?(): boolean;
8
+ /** Executes operator commands that must run inside the owning daemon. */
9
+ bindExistingRoot?(request: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome>;
7
10
  }
8
11
  export interface RunChatDaemonInternalDeps {
9
12
  processPid?: number;
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Request/response control channel for chat-daemon commands that must be
3
+ * executed *by* the running owner instead of terminating it.
4
+ *
5
+ * `control.json` is the lifecycle channel: every request the owner recognizes
6
+ * there ends its serving loop (stop/reload). Adopting an existing Slack root
7
+ * must not stop a healthy daemon, so operator commands travel on this separate
8
+ * per-request channel and are answered in place.
9
+ *
10
+ * Invariants:
11
+ * - Every request is addressed to an exact owner (`ownerId`, `pid`,
12
+ * `incarnation`, daemon `generation`). A daemon that is not that exact owner
13
+ * answers `owner_changed` and performs no work, so a replaced or restarted
14
+ * owner can never satisfy a request captured against its predecessor. The
15
+ * same tuple is re-proven inside the commit fence and echoed in the answer,
16
+ * so the submitter can verify who actually acted.
17
+ * - `<id>.response.json` is a single-winner arbitration object created with
18
+ * `O_CREAT|O_EXCL`. The serving daemon must win it *before* it commits, and a
19
+ * submitter that has given up must win it before reporting failure. Whoever
20
+ * loses learns so definitively, which removes the commit-vs-timeout race
21
+ * without any sleep: a definitive `cancelled` answer proves no dispatch can
22
+ * follow within this submission's race, and a lost cancellation is reported
23
+ * as `unknown` rather than as a failure the caller could act on. The
24
+ * arbitration is scoped to that race under the trust boundary below; it is
25
+ * not durable settlement across a crashed and resurrected daemon.
26
+ * - Documents live in an owner-only (`0700`) directory below the agent
27
+ * directory, are bounded, and are rejected unless they are ordinary regular
28
+ * files. This matches the product's existing same-UID local trust boundary;
29
+ * it is not a defence against a hostile process running as the same user.
30
+ * - Only identifiers travel through the channel. Tokens, message bodies, and
31
+ * control secrets are never written here.
32
+ *
33
+ * The channel proves *correlation*, never authorship: every field a response
34
+ * echoes is copied verbatim out of the plaintext request published beside it,
35
+ * so a definitive answer is only actionable once the caller corroborates the
36
+ * mutation against an authority outside this directory.
37
+ */
38
+ import { type ChatDaemonKind } from "./chat-daemon-control";
39
+ export declare const CHAT_DAEMON_COMMAND_VERSION = 1;
40
+ export declare const DEFAULT_CHAT_DAEMON_COMMAND_TIMEOUT_MS = 15000;
41
+ export type ChatDaemonCommandName = "bind-thread";
42
+ export type ChatDaemonCommandStatus = "ok" | "rejected" | "owner_changed" | "expired" | "outcome_unknown";
43
+ /** Exact owner authority a request is addressed to. */
44
+ export interface ChatDaemonCommandOwner {
45
+ ownerId: string;
46
+ pid: number;
47
+ incarnation: string;
48
+ generation: number;
49
+ }
50
+ export interface ChatDaemonCommandRequest extends ChatDaemonCommandOwner {
51
+ version: typeof CHAT_DAEMON_COMMAND_VERSION;
52
+ requestId: string;
53
+ kind: ChatDaemonKind;
54
+ command: ChatDaemonCommandName;
55
+ sessionId: string;
56
+ rootTs: string;
57
+ createdAt: number;
58
+ expiresAt: number;
59
+ }
60
+ /**
61
+ * The answer to exactly one request.
62
+ *
63
+ * The owner tuple, session, and root are the *addressed* request's, echoed
64
+ * verbatim. A response therefore carries the complete request envelope, and a
65
+ * submitter can prove that the document in front of it is correlated with its
66
+ * own request rather than with a replayed or concurrent one.
67
+ *
68
+ * That is correlation, not authentication. Every echoed field is public: it is
69
+ * copied out of the plaintext request published in the same command directory,
70
+ * so a stale or planted document can satisfy the envelope without the addressed
71
+ * daemon ever running. A caller that needs a definitive outcome must corroborate
72
+ * it against an authority outside this directory (the durable mutation itself).
73
+ */
74
+ export interface ChatDaemonCommandResponse extends ChatDaemonCommandOwner {
75
+ version: typeof CHAT_DAEMON_COMMAND_VERSION;
76
+ requestId: string;
77
+ kind: ChatDaemonKind;
78
+ command: ChatDaemonCommandName;
79
+ sessionId: string;
80
+ rootTs: string;
81
+ status: ChatDaemonCommandStatus;
82
+ /** Machine-readable rejection category; never a message body or credential. */
83
+ code?: string;
84
+ endpointGeneration?: number;
85
+ teamId?: string;
86
+ channelId?: string;
87
+ completedAt: number;
88
+ }
89
+ /**
90
+ * Result of one handler dispatch, carrying explicit commit certainty.
91
+ *
92
+ * A failure is never just a code. `rejected` asserts that no mapping changed, so
93
+ * the caller may be told the binding definitively failed. `unknown` asserts the
94
+ * opposite: commit authority was exercised and the mapping may already be
95
+ * applied, so no definitive rejection may be reported for it. Handlers state
96
+ * this directly instead of leaving the channel to infer it from error text.
97
+ */
98
+ export type ChatDaemonCommandOutcome = {
99
+ ok: true;
100
+ sessionId: string;
101
+ endpointGeneration: number;
102
+ teamId: string;
103
+ channelId: string;
104
+ rootTs: string;
105
+ } | {
106
+ ok: false;
107
+ certainty: "rejected";
108
+ code: string;
109
+ } | {
110
+ ok: false;
111
+ certainty: "unknown";
112
+ code: string;
113
+ };
114
+ export interface ChatDaemonCommandBindInput {
115
+ sessionId: string;
116
+ rootTs: string;
117
+ /**
118
+ * Terminal authority for this exact request. It must be awaited inside the
119
+ * store fence, immediately before the commit, and the commit must be
120
+ * abandoned when it answers `false`. It re-proves the exact daemon owner
121
+ * tuple, that the request is still published and unexpired, and takes the
122
+ * single-winner response claim, after which no cancellation can succeed.
123
+ */
124
+ commitAuthority?: () => Promise<boolean>;
125
+ }
126
+ /** Daemon-side executor. Implemented by the runtime that owns the live transports. */
127
+ export interface ChatDaemonCommandHandler {
128
+ bindExistingRoot(input: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome>;
129
+ }
130
+ export declare function isChatDaemonCommandRequest(value: unknown): value is ChatDaemonCommandRequest;
131
+ export declare function isChatDaemonCommandResponse(value: unknown): value is ChatDaemonCommandResponse;
132
+ /**
133
+ * The owner-only directory the channel exchanges its documents in.
134
+ *
135
+ * `directory` is a plain pathname: this codebase already trusts same-UID local
136
+ * processes, and every document below it is bounded and validated, so the
137
+ * channel does not attempt hostile-filesystem hardening it cannot honestly
138
+ * provide from TypeScript.
139
+ */
140
+ export interface ChatDaemonCommandScope {
141
+ readonly directory: string;
142
+ }
143
+ export interface OpenChatDaemonCommandScopeInput {
144
+ agentDir: string;
145
+ kind: ChatDaemonKind;
146
+ /** Creates the daemon and command directories owner-only when absent. */
147
+ create?: boolean;
148
+ }
149
+ /** Capture the command directory, or report the channel unusable. */
150
+ export declare function openChatDaemonCommandScope(input: OpenChatDaemonCommandScopeInput): Promise<ChatDaemonCommandScope | undefined>;
151
+ /** Build a request addressed to one exact owner. */
152
+ export declare function buildChatDaemonCommandRequest(input: {
153
+ kind: ChatDaemonKind;
154
+ command: ChatDaemonCommandName;
155
+ owner: ChatDaemonCommandOwner;
156
+ sessionId: string;
157
+ rootTs: string;
158
+ now?: number;
159
+ ttlMs?: number;
160
+ requestId?: string;
161
+ }): ChatDaemonCommandRequest;
162
+ export interface SubmitChatDaemonCommandInput {
163
+ agentDir: string;
164
+ kind: ChatDaemonKind;
165
+ owner: ChatDaemonCommandOwner;
166
+ command: ChatDaemonCommandName;
167
+ sessionId: string;
168
+ rootTs: string;
169
+ timeoutMs?: number;
170
+ pollIntervalMs?: number;
171
+ /** Bounded wait after a lost cancellation, before the outcome is reported unknown. */
172
+ settleGraceMs?: number;
173
+ ttlMs?: number;
174
+ now?: () => number;
175
+ sleep?: (ms: number) => Promise<void>;
176
+ requestId?: string;
177
+ }
178
+ /**
179
+ * Terminal result of one submission.
180
+ *
181
+ * `cancelled` is the only definitive failure: the submitter won the response
182
+ * claim, so the addressed owner cannot dispatch this request from here on within
183
+ * this submission's race. `unknown` means the daemon won that claim first and its
184
+ * answer was not observed; the caller must not report a definitive failure,
185
+ * because a commit may already be applied.
186
+ *
187
+ * `untrusted` means the response object under this identifier is a complete,
188
+ * well-formed document that does not carry this request's envelope. It is
189
+ * channel corruption — stale or planted — so it may not be read as an answer of
190
+ * any status, and because the identifier is now occupied the real outcome is
191
+ * unknowable from here; the caller must treat it exactly like `unknown`.
192
+ */
193
+ export type ChatDaemonCommandSubmission = {
194
+ outcome: "answered";
195
+ response: ChatDaemonCommandResponse;
196
+ } | {
197
+ outcome: "cancelled";
198
+ } | {
199
+ outcome: "unknown";
200
+ } | {
201
+ outcome: "untrusted";
202
+ code: "response_envelope_mismatch";
203
+ } | {
204
+ outcome: "unavailable";
205
+ code: "command_channel_unavailable" | "request_id_unavailable";
206
+ };
207
+ /**
208
+ * Publish a command for the addressed owner and wait for its answer.
209
+ *
210
+ * The wait ends in exactly one of four states, and the commit-vs-timeout race is
211
+ * arbitrated by the exclusive creation of the response object rather than by any
212
+ * timing assumption.
213
+ */
214
+ export declare function submitChatDaemonCommand(input: SubmitChatDaemonCommandInput): Promise<ChatDaemonCommandSubmission>;
215
+ export interface ServeChatDaemonCommandsInput {
216
+ agentDir: string;
217
+ kind: ChatDaemonKind;
218
+ /** The serving daemon's own proven authority. */
219
+ ownerId: string;
220
+ pid: number;
221
+ incarnation: string;
222
+ generation: number;
223
+ handler: ChatDaemonCommandHandler;
224
+ /**
225
+ * Re-proves that this process still holds the persisted owner record. It is
226
+ * checked before any work is dispatched *and again* inside the commit fence,
227
+ * so a daemon that lost ownership after dispatch mutates nothing.
228
+ */
229
+ verifyOwnership?: () => Promise<boolean>;
230
+ now?: () => number;
231
+ }
232
+ /**
233
+ * Answer every pending request addressed to this exact owner. Requests aimed at
234
+ * any other owner identity are answered `owner_changed` without doing work, and
235
+ * expired requests are answered `expired`; neither performs a mutation.
236
+ */
237
+ export declare function serveChatDaemonCommandsOnce(input: ServeChatDaemonCommandsInput): Promise<number>;
238
+ /**
239
+ * Answer every pending request against an already-captured command scope.
240
+ *
241
+ * Exposed so a caller that already holds the directory authority drives exactly
242
+ * the production path.
243
+ */
244
+ export declare function serveChatDaemonCommandsAgainstScope(scope: ChatDaemonCommandScope, input: ServeChatDaemonCommandsInput): Promise<number>;
@@ -32,7 +32,11 @@ export type ChatDaemonAction = "stop" | "reload";
32
32
  * Discord generation 24 / slack generation 23 apply provider-completeness and
33
33
  * effective-enable admission to chat daemon lifecycle controls. Discord
34
34
  * generation 25 / slack generation 24 apply identity-bound exact replacement
35
- * cleanup shared by managed-session and daemon filesystem authority.
35
+ * cleanup shared by managed-session and daemon filesystem authority. Discord
36
+ * generation 26 / slack generation 25 add the in-place operator command channel:
37
+ * an owner serves per-request commands inside its own serving loop and answers
38
+ * them against an exact owner tuple, so an owner at an earlier generation may
39
+ * not serve or answer a request captured against this contract.
36
40
  */
37
41
  export declare const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>>;
38
42
  export declare function chatDaemonGeneration(kind: ChatDaemonKind): number;
@@ -93,6 +97,12 @@ export declare function chatDaemonPaths(agentDir: string, kind: ChatDaemonKind):
93
97
  state: string;
94
98
  control: string;
95
99
  };
100
+ /**
101
+ * Configuration fingerprint that identifies which settings a daemon owner was
102
+ * started for. `undefined` means the current settings cannot configure that
103
+ * transport at all, so no owner can be authorized against them.
104
+ */
105
+ export declare function chatDaemonIdentity(settings: Settings, kind: ChatDaemonKind): string | undefined;
96
106
  export declare function readChatDaemonState(agentDir: string, kind: ChatDaemonKind): Promise<ChatDaemonState | undefined>;
97
107
  export declare function readChatDaemonControlRequest(agentDir: string, kind: ChatDaemonKind): Promise<ChatDaemonControlRequest | undefined>;
98
108
  export declare function writeChatDaemonControlRequest(agentDir: string, kind: ChatDaemonKind, request: ChatDaemonControlRequest): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import { SessionIndex } from "../broker/session-index";
2
2
  import { type SdkSessionEndpoint } from "../client/discovery";
3
+ import type { ChatDaemonCommandBindInput, ChatDaemonCommandOutcome } from "./chat-daemon-command-channel";
3
4
  import type { ChatDaemonKind } from "./chat-daemon-control";
4
5
  import type { DiscordProvider } from "./discord-provider";
5
6
  import { type SlackProviderClient } from "./slack-provider";
@@ -69,6 +70,12 @@ export declare class ChatDaemonRuntime {
69
70
  private reconcile;
70
71
  private attach;
71
72
  private resolveEndpoint;
73
+ /**
74
+ * Adopt an operator-supplied Slack root for one attached session. Returns a
75
+ * machine-readable rejection instead of throwing so the daemon control plane
76
+ * can answer without exposing internal failure detail.
77
+ */
78
+ bindExistingRoot(request: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome>;
72
79
  private schedule;
73
80
  private enqueueFrame;
74
81
  private handleFrame;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Typed control-plane discriminants that must never reach chat presentation.
3
+ *
4
+ * A chat daemon observes one session socket for user-visible events, but that
5
+ * socket also carries the SDK's own request/response traffic: `SdkClient`
6
+ * settles a pending request from an inbound frame and still forwards the very
7
+ * same frame to every `onFrame` observer. A runtime that projects unknown frame
8
+ * types into a generic notification therefore renders protocol answers — most
9
+ * visibly `GJC event replay result` — as chat content, and a chat transport
10
+ * with no mapping for that session publishes it as a brand-new root.
11
+ *
12
+ * Membership is exact, typed, and closed. It is never inferred from a `_result`
13
+ * or `_response` suffix: ordinary public frames are free to be named anything,
14
+ * and a fuzzy rule would silently swallow real user content. Every entry below
15
+ * names the source authority that emits it and the code path that proves it can
16
+ * be delivered to a chat daemon's frame observer.
17
+ *
18
+ * In scope (proven reachable at the chat-daemon session-client seam):
19
+ * - `event_replay_result` — `SessionSdkHost` (`sdk/host/host.ts`) answers the
20
+ * `event_replay` request that `ChatDaemonRuntime.attach()` issues on every
21
+ * attachment, so every attached session receives exactly this frame.
22
+ * - `control_response` — the same host's answer to the `control_request` that
23
+ * `ChatDaemonRuntime` sends for an authorized `/sdk control …` command, and
24
+ * the endpoint-stale refusal the session runtime writes for a fenced
25
+ * connection.
26
+ * - `query_response` — the host's answer to the matching `query_request`, and
27
+ * the same endpoint-stale refusal path.
28
+ * - `hello` — `ServerMessage::Hello` from the native session server
29
+ * (`crates/gjc-sdk/src/protocol.rs`), sent to every connection at accept time
30
+ * and re-observed by frame handlers whenever the client reconnects and
31
+ * adopts a new connection id.
32
+ *
33
+ * Deliberately out of scope — typed server→client frames that exist but are
34
+ * unicast only to a connection that issued a request a chat daemon never
35
+ * sends, so no source path delivers them here: `session_activate_result`,
36
+ * `register_provider_result`, `lease_state`, `reverse_response`,
37
+ * `reverse_request`, `reverse_cancel`, `global_response`, `pong`, and
38
+ * `transport_error`. Add one only together with the source path that proves it
39
+ * reaches this seam.
40
+ */
41
+ export declare const CONTROL_PLANE_FRAME_TYPES: ReadonlySet<string>;
42
+ /** Whether an exact frame/event discriminant is protocol-only control plane. */
43
+ export declare function isControlPlaneFrameType(name: string | undefined): boolean;
@@ -70,4 +70,22 @@ export declare class ConversationStore<T extends ConversationRecord> {
70
70
  delete(key: string, expectedGeneration: number): Promise<boolean>;
71
71
  /** Apply a synchronous update under the mapping lock, retrying no stale state. */
72
72
  transact(key: string, update: (current: T | undefined) => T | undefined): Promise<T | undefined>;
73
+ /**
74
+ * Apply one key update while atomically observing every mapping in the same
75
+ * store snapshot. The observation and the write share the cross-process file
76
+ * lock, so a caller can enforce uniqueness constraints that span keys.
77
+ *
78
+ * The update may be asynchronous so a caller can re-prove local authority
79
+ * (process, endpoint, or session state) inside the lock, immediately before
80
+ * its commit; returning `current` unchanged aborts without any mutation. It
81
+ * must never perform a remote/network call while holding the lock: the lock
82
+ * is a short fence, not a place to wait on a provider.
83
+ *
84
+ * Authority proven by the update is the last proof before the atomic
85
+ * replacement. Authority that rolls after it is an ordinary lifecycle event:
86
+ * the mapping records the endpoint generation it was proven under, so a newer
87
+ * generation fences it through the same monotonic compare-and-swap every
88
+ * other writer uses.
89
+ */
90
+ transactWithSnapshot(key: string, update: (current: T | undefined, conversations: Readonly<Record<string, T>>) => T | undefined | Promise<T | undefined>): Promise<T | undefined>;
73
91
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The prepare → bind → activate lifecycle for adopting an existing chat root.
3
+ *
4
+ * Stock startup publishes a session's readiness immediately, so the running
5
+ * chat daemon surfaces the session and creates its own root before an operator
6
+ * could ever name an existing one. A session that opts in is *prepared*
7
+ * instead: its id, endpoint, and broker registration are discoverable authority
8
+ * while readiness stays withheld, which leaves exactly one root claim for
9
+ * `gjc notify bind-thread` to fill. Activation then publishes readiness once,
10
+ * and the daemon adopts the bound root instead of replacing it.
11
+ *
12
+ * Nothing here mutates a mapping. The gate only reads the daemon-owned store to
13
+ * prove a binding exists, and the activation client only asks the session's own
14
+ * host to publish the readiness it withheld.
15
+ */
16
+ import type { SessionIndex } from "../broker/session-index";
17
+ import type { SdkSessionEndpoint, SdkSessionEndpointScope } from "../client/discovery";
18
+ import type { SessionActivationGate } from "../host";
19
+ import { type ActivatedPreparedSession, type PreparedSessionActivationClient } from "../session-activation";
20
+ import type { ConversationStore } from "./conversation-store";
21
+ import type { SlackConversation } from "./slack-conversation";
22
+ /** Explicit, session-scoped opt-in for withholding readiness until a root is bound. */
23
+ export declare const EXISTING_THREAD_BIND_ENV = "GJC_NOTIFY_BIND_EXISTING_THREAD";
24
+ /**
25
+ * Existing-thread binding is opt-in, and only the exact value `1` opts in.
26
+ *
27
+ * Every other value — absent, empty, `0`, or anything truthy-looking — keeps
28
+ * the stock immediate-readiness contract, so no session silently loses its
29
+ * root publication because of an ambiguous environment.
30
+ */
31
+ export declare function isExistingThreadBindingRequested(env?: NodeJS.ProcessEnv): boolean;
32
+ export interface SlackBindingActivationInput {
33
+ store: ConversationStore<SlackConversation>;
34
+ teamId: string;
35
+ channelId: string;
36
+ }
37
+ /**
38
+ * Authorize activation only against an applied binding for this exact session.
39
+ *
40
+ * The proof is the daemon-owned mapping itself: an active record for this
41
+ * session, in the configured workspace and channel, carrying a root and the
42
+ * exact endpoint generation being activated. A missing, foreign, or
43
+ * stale-generation mapping is a refusal, and an unreadable store raises so the
44
+ * caller reports unavailable authority instead of an authorization.
45
+ */
46
+ export declare function createSlackBindingActivationGate(input: SlackBindingActivationInput): SessionActivationGate;
47
+ export type { ActivatedPreparedSession, PreparedSessionActivationClient, SessionActivationErrorCode, } from "../session-activation";
48
+ export { requestPreparedSessionActivation, SessionActivationError } from "../session-activation";
49
+ export interface PreparedSessionActivationInput {
50
+ sessionIndex: SessionIndex;
51
+ sessionId: string;
52
+ }
53
+ export interface PreparedSessionActivationDeps {
54
+ readEndpoint?: (repo: string, sessionId: string, scope?: SdkSessionEndpointScope) => Promise<SdkSessionEndpoint | null>;
55
+ connect?: (endpoint: {
56
+ url: string;
57
+ token: string;
58
+ }) => Promise<PreparedSessionActivationClient>;
59
+ }
60
+ /**
61
+ * Ask a prepared session to publish the readiness it withheld.
62
+ *
63
+ * The same discovery authority the binding requires is resolved first, so an
64
+ * unindexed, terminated, or endpoint-rolled session is refused before any
65
+ * connection is opened. The request names the exact session and endpoint
66
+ * generation, and the answer is accepted only when it proves both, so a
67
+ * replacement session can never be activated in place of the requested one.
68
+ * Activation is idempotent: an exact retry answers `already` without a second
69
+ * readiness signal.
70
+ */
71
+ export declare function activatePreparedSession(input: PreparedSessionActivationInput, deps?: PreparedSessionActivationDeps): Promise<ActivatedPreparedSession>;
@@ -43,6 +43,16 @@ export declare function truncateTelegramHtml(message: string, max?: number, mark
43
43
  export declare function splitTelegramHtml(message: string, max?: number): string[];
44
44
  /** Finalize an optional message: undefined passthrough, else safe-truncate. */
45
45
  export declare function finalizeTelegramHtml(message?: string): string | undefined;
46
+ /** Multi-select state marker a transport may prepend to an option label. */
47
+ export declare const SELECTION_MARK_CHECKED = "\u2611";
48
+ export declare const SELECTION_MARK_UNCHECKED = "\u2610";
49
+ /**
50
+ * Drop a leading `N.`/`N)` index already embedded in an option label (e.g.
51
+ * deep-interview options pre-numbered by the ask tool) so the canonical
52
+ * one-based index can be applied instead. A selection marker stays in front of
53
+ * the text: the label is renumbered, not un-marked.
54
+ */
55
+ export declare function renumberableOptionText(label: string): string;
46
56
  /**
47
57
  * One-based, plain-text button label (Telegram does not parse HTML in labels).
48
58
  *
@@ -51,6 +51,16 @@ export interface NotificationExactUnlinkResult {
51
51
  export declare function readNotificationEndpointFile(file: string): Promise<NotificationEndpointFile>;
52
52
  /** Bump when the native exact-deletion identity contract changes. */
53
53
  export declare const NATIVE_PATH_IDENTITY_CONTRACT_VERSION = 1;
54
+ /**
55
+ * Remove exactly the inspected regular file.
56
+ *
57
+ * Intermediate directory symlinks (e.g. multi-account layouts that share
58
+ * `notifications/` via a directory symlink) are resolved before the native
59
+ * no-follow unlink. Only the parent path is canonicalized; the final basename
60
+ * is rejoined so native exact_unlink still applies AT_SYMLINK_NOFOLLOW to the
61
+ * final path component at mutation time. A final-component file symlink stays
62
+ * `reparse_point` (including TOCTOU replacement after JS preflight).
63
+ */
54
64
  export declare function exactUnlinkNotificationFile(file: string, identity: NotificationEndpointFileIdentity, quarantineName: string): NotificationExactUnlinkResult;
55
65
  /** Minimal filesystem surface the service needs; injectable for tests. */
56
66
  export interface NotificationServiceFs {