@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
@@ -1,6 +1,8 @@
1
1
  import { type SdkSessionEndpoint } from "../client/discovery";
2
2
  import { ConversationStore } from "./conversation-store";
3
3
  import { type SlackConversation } from "./slack-conversation";
4
+ export type { SlackThreadBindingErrorCode } from "./slack-thread-binding";
5
+ export { SlackThreadBindingError } from "./slack-thread-binding";
4
6
  export declare class SlackEndpointBindingError extends Error {
5
7
  constructor(message?: string);
6
8
  }
@@ -11,6 +13,11 @@ export interface SlackEndpoint extends SdkSessionEndpoint {
11
13
  export interface SlackSdkClient {
12
14
  send(frame: Record<string, unknown>): void;
13
15
  }
16
+ /** Proven right to bind one session at one endpoint generation. */
17
+ export interface SlackBindingAuthority {
18
+ sessionId: string;
19
+ endpointGeneration: number;
20
+ }
14
21
  export interface SlackNotificationDaemonOptions {
15
22
  agentDir: string;
16
23
  repo: string;
@@ -27,6 +34,12 @@ export interface SlackNotificationDaemonOptions {
27
34
  publicationOwnerId?: string;
28
35
  publicationLeaseMs?: number;
29
36
  resolveEndpoint?: (sessionId: string) => Promise<SlackEndpoint | null>;
37
+ /**
38
+ * Exact discovery/attachment authority for adopting an existing root. The
39
+ * runtime supplies index, endpoint, and attachment proof; without it only the
40
+ * resolvable endpoint generation can be proven.
41
+ */
42
+ resolveBindingAuthority?: (sessionId: string) => Promise<SlackBindingAuthority | undefined>;
30
43
  createClient: (endpoint: SlackEndpoint) => SlackSdkClient;
31
44
  onCommand?: (sessionId: string, content: string, endpoint: SlackEndpoint, idempotencyKey: string) => Promise<boolean>;
32
45
  }
@@ -46,6 +59,20 @@ export declare class SlackNotificationDaemon {
46
59
  /** Accepted inbound effects are durably claimed before their Socket Mode ACK. */
47
60
  handleEnvelope(envelope: SlackSocketEnvelope): Promise<boolean>;
48
61
  postRoot(sessionId: string, body: string, endpointGeneration?: number): Promise<SlackConversation>;
62
+ /**
63
+ * Adopt an operator-supplied root in the configured workspace and channel
64
+ * without publishing a replacement root.
65
+ *
66
+ * The root is verified against the provider before any lock is taken, and the
67
+ * claim re-proves session authority inside the store lock. When the caller is
68
+ * the daemon command channel it also supplies `commitAuthority`, which
69
+ * re-proves the exact daemon owner tuple and takes terminal request
70
+ * authority in the same fence, so an ownership change or a cancellation
71
+ * between dispatch and commit leaves the store untouched. The claim targets
72
+ * the same session key stock publication uses, so a concurrent first
73
+ * notification observes the adopted root instead of posting a second one.
74
+ */
75
+ bindExistingRoot(sessionId: string, rootTs: string, commitAuthority?: () => Promise<boolean>): Promise<SlackConversation>;
49
76
  /** Deliver a notification into the mapped root thread, creating that root once. */
50
77
  notify(sessionId: string, body: string, actionId?: string, endpointGeneration?: number): Promise<SlackConversation>;
51
78
  /** Posts a safe command outcome to the active mapped root thread. */
@@ -63,4 +63,15 @@ export declare class SlackLiveProvider implements SlackProviderClient, SlackDiag
63
63
  clientMsgId: string;
64
64
  threadTs?: string;
65
65
  }): Promise<SlackMessageSearchResult | null>;
66
+ /**
67
+ * Confirm that `ts` addresses a real message in `channel`. Slack answers
68
+ * `conversations.replies` with the addressed message first; a timestamp that
69
+ * is unknown, deleted, or lives in another channel produces a Web API error
70
+ * instead, which surfaces as a `SlackProviderError` the caller treats as an
71
+ * unverified root.
72
+ */
73
+ findMessageByTimestamp(input: {
74
+ channel: string;
75
+ ts: string;
76
+ }): Promise<SlackMessageSearchResult | null>;
66
77
  }
@@ -50,6 +50,15 @@ export interface SlackProviderClient {
50
50
  clientMsgId: string;
51
51
  threadTs?: string;
52
52
  }): Promise<SlackMessageSearchResult | null>;
53
+ /**
54
+ * Prove that an operator-supplied timestamp addresses a real message in the
55
+ * requested channel. Adoption of an existing root is refused when a client
56
+ * cannot answer this, so verification is never silently skipped.
57
+ */
58
+ findMessageByTimestamp?(input: {
59
+ channel: string;
60
+ ts: string;
61
+ }): Promise<SlackMessageSearchResult | null>;
53
62
  readonly transportHealthy?: boolean;
54
63
  }
55
64
  /**
@@ -74,4 +83,9 @@ export declare class SlackProvider {
74
83
  clientMsgId: string;
75
84
  threadTs?: string;
76
85
  }): Promise<SlackMessageSearchResult | null>;
86
+ /** Fail-closed root verification: a client without this capability can never confirm a root. */
87
+ findMessageByTimestamp(input: {
88
+ channel: string;
89
+ ts: string;
90
+ }): Promise<SlackMessageSearchResult | null>;
77
91
  }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Adoption of an operator-supplied Slack thread as a live session's root.
3
+ *
4
+ * Three separate authorities have to agree before a mapping exists:
5
+ * 1. *Configuration*: a complete Slack target (both tokens plus workspace and
6
+ * channel) must already be configured; binding never accepts a target.
7
+ * 2. *Daemon*: only the running owner may mutate mappings, and only while it is
8
+ * still the exact owner (`ownerId`/`pid`/`incarnation`/daemon generation)
9
+ * captured before the request was published.
10
+ * 3. *Session*: the SDK session must be attachable right now — indexed, live,
11
+ * non-terminal, with a readable, non-stale endpoint whose pid matches the
12
+ * indexed host and whose generation has not rolled.
13
+ *
14
+ * Provider verification happens before any lock is taken, and the mapping commit
15
+ * re-proves session authority inside the store lock, so an authority that
16
+ * changes mid-flight leaves no mapping behind.
17
+ */
18
+ import type { Settings } from "../../config/settings";
19
+ import type { SessionIndex } from "../broker/session-index";
20
+ import { type SdkSessionEndpoint, type SdkSessionEndpointScope } from "../client/discovery";
21
+ import { type EnsureChatDaemonResult } from "./chat-daemon-control";
22
+ import { ConversationStore } from "./conversation-store";
23
+ import { type SlackConversation } from "./slack-conversation";
24
+ export type SlackThreadBindingErrorCode = "invalid_root" | "target_not_configured" | "daemon_unavailable" | "daemon_owner_changed" | "session_not_live" | "root_not_found" | "provider_unavailable" | "root_conflict" | "session_conflict" | "binding_outcome_unknown" | "binding_failed";
25
+ /** A fail-closed rejection while adopting an operator-supplied Slack thread. */
26
+ export declare class SlackThreadBindingError extends Error {
27
+ readonly code: SlackThreadBindingErrorCode;
28
+ constructor(code: SlackThreadBindingErrorCode, message: string);
29
+ }
30
+ export declare function isBoundedSlackRootTs(value: string): boolean;
31
+ /** Reject a non-addressable root before any authority read or persistence. */
32
+ export declare function assertBoundedSlackRootTs(rootTs: string): void;
33
+ export interface SlackSessionBindingAuthority {
34
+ sessionId: string;
35
+ endpointGeneration: number;
36
+ pid: number;
37
+ repo: string;
38
+ /**
39
+ * The exact endpoint this authority was proven against. Callers must use it
40
+ * rather than re-reading discovery: a second read can observe a different
41
+ * process whose endpoint generation happens to repeat, and the numeric
42
+ * generation alone cannot tell the two apart.
43
+ */
44
+ endpoint: SdkSessionEndpoint;
45
+ scope: SdkSessionEndpointScope;
46
+ endpointMtimeMs: number;
47
+ }
48
+ export interface SessionBindingAuthorityInput {
49
+ sessionIndex: SessionIndex;
50
+ sessionId: string;
51
+ readEndpoint?: (repo: string, sessionId: string, scope?: SdkSessionEndpointScope) => Promise<SdkSessionEndpoint | null>;
52
+ statEndpoint?: (endpointPath: string) => Promise<{
53
+ mtimeMs: number;
54
+ } | undefined>;
55
+ }
56
+ /**
57
+ * Resolve exact discovery/attachment authority for one session.
58
+ *
59
+ * `IndexedSession.live` alone is only pid liveness, which a terminated or
60
+ * unregistered session can still satisfy through pid reuse or a stale record.
61
+ * Adoption additionally requires an intact index replay, a non-terminal record,
62
+ * and a discovery endpoint that is present, well-formed, not marked stale, and
63
+ * owned by the same host pid the index recorded.
64
+ */
65
+ export declare function resolveSessionBindingAuthority(input: SessionBindingAuthorityInput): Promise<SlackSessionBindingAuthority | undefined>;
66
+ export interface SlackThreadClaimInput {
67
+ store: ConversationStore<SlackConversation>;
68
+ /** The session root-claim key shared with stock root publication. */
69
+ key: string;
70
+ teamId: string;
71
+ channelId: string;
72
+ sessionId: string;
73
+ rootTs: string;
74
+ endpointGeneration: number;
75
+ /**
76
+ * Re-proves session authority inside the store lock, immediately before
77
+ * commit. It may be invoked more than once for one claim: authority here is
78
+ * monotone — a rolled endpoint generation, a replaced daemon owner tuple and
79
+ * a retired single-winner request claim never come back — so repeating the
80
+ * proof can only ever narrow the window, never widen it. It must therefore be
81
+ * idempotent and must never perform a remote call.
82
+ */
83
+ revalidate: () => Promise<boolean>;
84
+ now?: () => number;
85
+ }
86
+ /**
87
+ * Claim an existing Slack root for a session without publishing a replacement.
88
+ *
89
+ * The claim transacts the same `intent:<sessionId>` key that stock root
90
+ * publication uses, so a bind and a concurrent first notification serialize on
91
+ * one invariant: whichever commits first owns the session's single root, and
92
+ * the loser observes it instead of creating a second one.
93
+ */
94
+ export declare function claimSlackThreadBinding(input: SlackThreadClaimInput): Promise<SlackConversation>;
95
+ export interface ConfiguredSlackThreadBindingInput {
96
+ settings: Settings;
97
+ sessionId: string;
98
+ threadTs: string;
99
+ }
100
+ export interface ConfiguredSlackThreadBindingDeps {
101
+ ensureDaemon?: (settings: Settings) => Promise<EnsureChatDaemonResult>;
102
+ timeoutMs?: number;
103
+ pollIntervalMs?: number;
104
+ /** Bounded wait for a daemon that took terminal authority before the caller gave up. */
105
+ settleGraceMs?: number;
106
+ now?: () => number;
107
+ sleep?: (ms: number) => Promise<void>;
108
+ }
109
+ /** Safe confirmation of an applied binding. It carries identifiers only. */
110
+ export interface BoundSlackThread {
111
+ sessionId: string;
112
+ endpointGeneration: number;
113
+ teamId: string;
114
+ channelId: string;
115
+ rootTs: string;
116
+ ownerId: string;
117
+ daemonGeneration: number;
118
+ }
119
+ /**
120
+ * Adopt an existing Slack root for a live session through the running daemon.
121
+ *
122
+ * The CLI never writes the mapping store: it proves the configured target and
123
+ * the exact current owner, then asks that owner to perform the mutation. A
124
+ * daemon that is no longer the captured owner answers `owner_changed`, so a
125
+ * replacement or restart between capture and execution can never apply the
126
+ * request.
127
+ *
128
+ * A submission that is not answered is cancelled through the channel's
129
+ * single-winner arbitration before it is reported as a failure. If that
130
+ * cancellation loses to the daemon's own commit authority, the outcome is
131
+ * reported as unknown instead of as a failure, because a mapping may already
132
+ * exist; re-running the same command observes the settled state idempotently.
133
+ *
134
+ * The command channel proves *correlation*, never authorship: every field a
135
+ * response echoes is copied verbatim out of the plaintext request published
136
+ * beside it. A definitive answer is therefore reported only after this caller
137
+ * itself observes the exact mutation in the conversation store — see
138
+ * `corroborateBoundMapping`.
139
+ */
140
+ export declare function bindConfiguredSlackThread(input: ConfiguredSlackThreadBindingInput, deps?: ConfiguredSlackThreadBindingDeps): Promise<BoundSlackThread>;
@@ -23,6 +23,8 @@ export interface RunDaemonInternalDeps {
23
23
  clearInterval?: (timer: Timer) => void;
24
24
  /** Reads persisted daemon ownership state; defaults to the real reader. */
25
25
  readDaemonState?: (settings: Settings) => Promise<DaemonState | undefined>;
26
+ /** Loads the verified machine-local identity; injectable so daemon tests do not touch the host. */
27
+ loadInstallationHostId?: () => Promise<string>;
26
28
  }
27
29
  export declare function createLightweightDaemonSettings(input: {
28
30
  agentDir: string;
@@ -53,15 +53,36 @@ export declare const NOTIFICATION_PROTOCOL_VERSION = 3;
53
53
  * ownership, settlement, and descendant cleanup authority. Generation 38 also
54
54
  * adds durable provider-intent admission without changing owner, reclaim,
55
55
  * signal, or spawn authority.
56
+ * Generation 41 applies first-class provider-settings admission to Telegram
57
+ * lifecycle controls, plus cross-host topic-registry CAS convergence, host-and-epoch
58
+ * archive fencing, retained topic history, user-topic adoption provenance, and
59
+ * exact versionless shared-state upgrades with quarantined source snapshots.
56
60
  * Generation 42 applies first-class provider-settings admission to Telegram
57
61
  * lifecycle controls. Generation 43 applies identity-bound exact replacement
58
- * cleanup shared by managed-session and daemon filesystem authority.
62
+ * cleanup shared by managed-session and daemon filesystem authority. Generation
63
+ * 44 makes callback recovery restart-safe by revoking persisted routes and
64
+ * callback receipts durable before routing and binds aliases to exact asks.
65
+ * Generation 46 stages accepted callback activation and makes callback
66
+ * consumption transactional under exact pending and lease authority. Generation
67
+ * 47 settles failed staged revocation and guards the complete callback authority
68
+ * and polling dependency chain. Generation 48 uses crash-durable callback
69
+ * receipts, a legacy-disjoint random alias namespace, and exact topic leases.
70
+ * Generation 49 drains every admitted session-message handler before final
71
+ * durable persistence and ownership release. Generation 50 resolves intermediate
72
+ * notifications-directory symlinks before native exact unlink while keeping
73
+ * final-component file symlinks fail-closed under AT_SYMLINK_NOFOLLOW (bounded
74
+ * #3761 multi-account activation repair). Generation 51 adds shared durable
75
+ * topic authority, archive recovery, and requires Telegram's documented error
76
+ * code for idempotent archive settlement. Generation 52 is claimed by the
77
+ * pre-readiness daemon-child exit diagnostics slice (#3761). Generation 53
78
+ * renders multi-select state for ask-tool asks, not only durable workflow
79
+ * gates, and renumbers pre-numbered options exactly once around the selection
80
+ * marker.
59
81
  */
60
- export declare const DAEMON_GENERATION = 43;
82
+ export declare const DAEMON_GENERATION = 53;
61
83
  /**
62
- * Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers
63
- * all builds published before this field existed; epoch 2 covered generation 29;
64
- * epoch 3 covered generation 30; bump this to force serving convergence and
65
- * reload of compatible live predecessors.
84
+ * Serving-compatibility boundary for daemon lifecycle requests. Epoch 5
85
+ * requires the complete generation-36 topic authority contract, so older
86
+ * epoch-4 daemons cannot keep serving across an upgrade.
66
87
  */
67
- export declare const SERVING_EPOCH = 4;
88
+ export declare const SERVING_EPOCH = 5;
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { type NativeExactFileIdentity, type NativeExactUnlinkResult } from "@gajae-code/natives";
2
3
  import type { Settings } from "../../config/settings";
3
4
  import type { DaemonRuntimeInfo } from "../../daemon/control-types";
4
5
  import { HEARTBEAT_TTL_MS } from "./daemon-paths";
@@ -10,7 +11,7 @@ import { type ControlServerLike, type LifecycleControlServerFactory } from "./li
10
11
  import type { OrchestratorDeps } from "./lifecycle-orchestrator";
11
12
  import { NotificationOperatorRuntime } from "./operator-runtime";
12
13
  import { type AliasTable, type CallbackRoute } from "./telegram-reference";
13
- import { type TopicEndpointBinding } from "./topic-registry";
14
+ import { type TopicEndpointBinding, type TopicRegistryCasAuthority, type TopicRegistryState } from "./topic-registry";
14
15
  export type EnsureDaemonResult = "owner_spawned" | "attached" | "disabled" | "blocked";
15
16
  /** Detailed result for orchestration that must distinguish a #2028 handoff from a fresh spawn. */
16
17
  export type EnsureTelegramDaemonDetailedResult = "spawned" | "reloaded" | "attached" | "disabled" | "blocked_identity";
@@ -45,6 +46,14 @@ export interface DaemonState {
45
46
  servingEpoch?: number;
46
47
  stoppedAt?: number;
47
48
  }
49
+ interface ExactFileStat {
50
+ dev: bigint;
51
+ ino: bigint;
52
+ nlink: bigint;
53
+ size: bigint;
54
+ mtimeNs: bigint;
55
+ isFile(): boolean;
56
+ }
48
57
  export interface TelegramDaemonFs {
49
58
  mkdir(path: string, opts?: fs.MakeDirectoryOptions): Promise<void>;
50
59
  readFile(path: string, encoding: BufferEncoding): Promise<string>;
@@ -52,10 +61,14 @@ export interface TelegramDaemonFs {
52
61
  rename(oldPath: string, newPath: string): Promise<void>;
53
62
  unlink(path: string): Promise<void>;
54
63
  open(path: string, flags: string, mode?: number): Promise<{
64
+ sync?: () => Promise<void>;
55
65
  close(): Promise<void>;
56
66
  }>;
57
67
  readdir(path: string): Promise<string[]>;
58
68
  chmod(path: string, mode: number): Promise<void>;
69
+ /** Crash-atomic persistence seams. Implementations without them fail closed. */
70
+ fsyncFile?(path: string): Promise<void>;
71
+ fsyncDirectory?(path: string): Promise<void>;
59
72
  stat?(path: string): Promise<{
60
73
  mtimeMs: number;
61
74
  size?: number;
@@ -64,8 +77,11 @@ export interface TelegramDaemonFs {
64
77
  /** Hard-link count; required to prove a staging temp has no second name. */
65
78
  nlink?: number;
66
79
  ctimeMs?: number;
67
- isDirectory?: () => boolean;
80
+ isDirectory?(): boolean;
68
81
  }>;
82
+ lstat?(path: string, opts: {
83
+ bigint: true;
84
+ }): Promise<ExactFileStat>;
69
85
  readEndpointFile?(path: string): Promise<NotificationEndpointFile>;
70
86
  exactUnlink?(path: string, identity: NotificationEndpointFileIdentity, quarantineName?: string): Promise<NotificationExactUnlinkResult>;
71
87
  }
@@ -151,6 +167,49 @@ export declare function startDaemonLifecycleControl(input: {
151
167
  auditRedactionKey: Uint8Array;
152
168
  env?: NodeJS.ProcessEnv;
153
169
  }): OrchestratorDeps;
170
+ export declare class TopicRegistryDurabilityUnavailableError extends Error {
171
+ readonly code = "durability_unavailable";
172
+ constructor(cause: unknown);
173
+ }
174
+ type TopicRegistryExactReplace = (sourcePath: string, destinationPath: string, expectedSource: NativeExactFileIdentity, expectedDestination: NativeExactFileIdentity) => NativeExactUnlinkResult;
175
+ /** Publish topic authority only after its staged bytes and replacement are durable. */
176
+ export declare function writeTopicRegistryAtomic(fsImpl: TelegramDaemonFs, file: string, data: unknown, platform?: NodeJS.Platform, exactReplace?: TopicRegistryExactReplace, expectedDestination?: NativeExactFileIdentity): Promise<void>;
177
+ /**
178
+ * Shared-volume topic authority backed by the existing cross-process file lock.
179
+ * A missing authority is the only valid bootstrap state; malformed and future
180
+ * snapshots are never interpreted as empty state.
181
+ */
182
+ export declare class FilesystemTopicRegistryCasAuthority implements TopicRegistryCasAuthority {
183
+ #private;
184
+ private readonly file;
185
+ private readonly fsImpl;
186
+ private readonly platform;
187
+ private readonly exactReplace;
188
+ private readonly installationHostId;
189
+ constructor(file: string, input: {
190
+ installationHostId: string;
191
+ fs?: TelegramDaemonFs;
192
+ platform?: NodeJS.Platform;
193
+ exactReplace?: TopicRegistryExactReplace;
194
+ });
195
+ read(): Promise<TopicRegistryState>;
196
+ compareAndSet(expectedGeneration: number, next: TopicRegistryState): Promise<boolean>;
197
+ private readLocked;
198
+ }
199
+ export interface MachineIdentityDeps {
200
+ platform?: NodeJS.Platform;
201
+ readFile?: (file: string) => Promise<string>;
202
+ runCommand?: (command: string, args: readonly string[]) => {
203
+ exitCode: number;
204
+ stdout: Uint8Array;
205
+ };
206
+ }
207
+ /** @internal */
208
+ export declare function parseWindowsMachineGuid(output: string): string | undefined;
209
+ /** @internal */
210
+ export declare function parseMacPlatformUuid(output: string): string | undefined;
211
+ /** Loads a verified machine-local identity without persisting the underlying machine ID. */
212
+ export declare function loadInstallationHostId(deps?: MachineIdentityDeps): Promise<string>;
154
213
  export declare function tryCreateOwnershipLock(fsImpl: TelegramDaemonFs, file: string, initialization: OwnershipLockMetadata): Promise<boolean>;
155
214
  type OwnershipLockMetadata = {
156
215
  pid: number;
@@ -552,6 +611,8 @@ export interface TelegramSpawnOwnerInput {
552
611
  roots?: string[];
553
612
  tokenFingerprint: string;
554
613
  chatId: string;
614
+ /** Ephemeral outbound-only validation destination for this owner launch. */
615
+ validationTestSupergroupChatId?: string;
555
616
  }
556
617
  export interface TelegramSpawnAcquisition {
557
618
  readonly ownerId: string;
@@ -587,6 +648,7 @@ export declare function buildTelegramDaemonSpawnArgs(input: {
587
648
  execPath?: string;
588
649
  ownerId: string;
589
650
  agentDir: string;
651
+ validationTestSupergroupChatId?: string;
590
652
  }): {
591
653
  command: string;
592
654
  args: string[];
@@ -732,6 +794,12 @@ export interface TelegramDaemonOptions {
732
794
  ownerId: string;
733
795
  botToken: string;
734
796
  chatId: string;
797
+ /**
798
+ * Exact ephemeral forum destination supplied to the daemon-internal validation
799
+ * command. Production ownership, pairing, and durable topic authority remain
800
+ * bound to `chatId`.
801
+ */
802
+ validationTestSupergroupChatId?: string;
735
803
  apiBase?: string;
736
804
  fetchImpl?: typeof fetch;
737
805
  fs?: TelegramDaemonFs;
@@ -793,6 +861,13 @@ export interface TelegramDaemonOptions {
793
861
  topics?: {
794
862
  nameTemplate?: string;
795
863
  };
864
+ /**
865
+ * Optional compare-and-set store for installations that share topic state
866
+ * across hosts. When configured, every publication is fenced by it.
867
+ */
868
+ topicRegistryAuthority?: TopicRegistryCasAuthority;
869
+ /** Stable host-local identity. Required whenever a shared authority is configured. */
870
+ installationHostId?: string;
796
871
  }
797
872
  interface SessionSocket {
798
873
  /** Immutable key of the transport endpoint that owns this socket. */
@@ -841,6 +916,9 @@ export declare class TelegramNotificationDaemon {
841
916
  private readonly opts;
842
917
  readonly aliasTable: AliasTable;
843
918
  readonly messageRoutes: Map<string | number, CallbackRoute | Omit<CallbackRoute, "answer">>;
919
+ /** Restart-revoked aliases retained only to terminalize their old keyboards on an authoritative replay. */
920
+ private readonly reissueBacklog;
921
+ private aliasPersistenceQueue;
844
922
  /** Telegram message id backing each streamed `${sessionId}:${coalesceKey}`, for in-place edits. */
845
923
  private readonly liveMessages;
846
924
  /** Endpoint-bound ownership for visible or dispatching tool bubbles. */
@@ -868,6 +946,8 @@ export declare class TelegramNotificationDaemon {
868
946
  private readonly botApi;
869
947
  private readonly effects;
870
948
  private readonly topics;
949
+ /** Stable host-local identity; never persisted in shared topic authority. */
950
+ private installationHostId;
871
951
  /** Serializes registry snapshots so an older atomic write cannot overwrite newer rename state. */
872
952
  /** Legacy sockets have no durable token, so explicit teardown is their revocation fence. */
873
953
  private readonly droppedSessions;
@@ -876,6 +956,8 @@ export declare class TelegramNotificationDaemon {
876
956
  private recoveryBindingClaimQueue;
877
957
  /** Durable compensation fences retry under supervision until persistence succeeds. */
878
958
  private readonly compensationFenceRetries;
959
+ /** All archive paths for one session share one durable fence and remote dispatch. */
960
+ private readonly archiveFlights;
879
961
  /** Daemon edit attempts that can race an accepted user service message. */
880
962
  private readonly daemonRenameAttempts;
881
963
  private readonly selectedAckPending;
@@ -906,8 +988,10 @@ export declare class TelegramNotificationDaemon {
906
988
  private threadedFallbackNoticeSent;
907
989
  /** Sessions whose identity header was already sent flat (Threaded Mode off). */
908
990
  private readonly flatIdentitySent;
909
- /** Cached result of whether the paired chat is a private chat (flat-fallback gate). */
910
- private pairedChatPrivate;
991
+ /** Cached delivery boundary for the private owner chat or validation forum. */
992
+ private pairedChatPrivacy;
993
+ /** Latched once Telegram confirms this chat cannot host forum topics. */
994
+ private topicCapabilityRefused;
911
995
  /** Bot username from getMe, cached once at owner startup for group/forum command targeting. */
912
996
  private botUsername;
913
997
  /** Sessions whose agent loop is currently busy (drives the typing indicator). */
@@ -972,6 +1056,7 @@ export declare class TelegramNotificationDaemon {
972
1056
  /** Map a lifecycle response/error to a user-facing message (G010 surfacing). */
973
1057
  private formatLifecycleResponse;
974
1058
  private callBotApi;
1059
+ private validationTopicDestination;
975
1060
  private readonly callBotApiClassified;
976
1061
  private botApiWithOutcomeCollector;
977
1062
  constructor(opts: TelegramDaemonOptions);
@@ -1005,6 +1090,12 @@ export declare class TelegramNotificationDaemon {
1005
1090
  private beginToolActivityShutdown;
1006
1091
  private dropSession;
1007
1092
  private deleteMessageRoutes;
1093
+ revokeCallbackAliases(socketLease: {
1094
+ session: SessionSocket;
1095
+ token: number;
1096
+ logicalSessionId: string;
1097
+ }): void;
1098
+ private reissuePendingAction;
1008
1099
  private static readonly THREADED_FRAMES;
1009
1100
  private topicNameFor;
1010
1101
  /**
@@ -1054,20 +1145,19 @@ export declare class TelegramNotificationDaemon {
1054
1145
  private topicPastOrphanGrace;
1055
1146
  private observeOrphanedTopic;
1056
1147
  /** Best-effort delete of a session topic once its local notification endpoint shuts down. */
1057
- private deleteTopic;
1148
+ /** Join all close, compensation, orphan, and restart callers for one session. */
1149
+ private archiveTopic;
1058
1150
  private persistTopics;
1059
1151
  loadTopics(): Promise<void>;
1060
1152
  /**
1061
- * Rehydrate durable adoption intents after restart, then reconcile: a
1062
- * non-expired sidecar whose target session already has a committed topic
1063
- * record is a stale leftover (commit succeeded but sidecar cleanup failed)
1064
- * and is removed safely without touching the user topic.
1153
+ * Rehydrate durable adoption intents after restart. A sidecar whose topic
1154
+ * already committed is stale evidence and can be removed without touching
1155
+ * the retained user-created topic.
1065
1156
  */
1066
1157
  loadAdoptionIntents(): Promise<void>;
1067
- /** Periodic sweep of expired adoption-intent sidecars (files only; no Telegram API). */
1068
1158
  private startAdoptionSweepTimer;
1069
1159
  private stopAdoptionSweepTimer;
1070
- /** Retry crash-interrupted topic deletes; only a definite Telegram result clears the durable fence. */
1160
+ /** Retry crash-interrupted topic archives only when the durable backoff permits it. */
1071
1161
  private reconcilePendingTopicDeletes;
1072
1162
  /** Download one Telegram file with the Bot API's 20 MiB ceiling and one end-to-end deadline. */
1073
1163
  private downloadTelegramFile;
@@ -1122,13 +1212,13 @@ export declare class TelegramNotificationDaemon {
1122
1212
  */
1123
1213
  private deliverFlatFallback;
1124
1214
  /**
1125
- * Resolve (and cache definitive resolution of) whether the paired `chatId` is
1126
- * a private chat. Topic and flat delivery are only safe in a private DM; an
1127
- * indeterminate `getChat` result fails closed for this attempt and is retried
1128
- * later.
1215
+ * Resolve and cache the outbound delivery boundary. The validation forum is
1216
+ * explicit and topic-only; all inbound and flat paths remain private-only.
1129
1217
  */
1218
+ private validationMode;
1130
1219
  private resolvePairedChatPrivacy;
1131
- /** Keep existing outbound callers fail-closed for indeterminate privacy. */
1220
+ private pairedChatAllowsTopics;
1221
+ /** Keep all flat delivery and inbound control paths private-only. */
1132
1222
  private pairedChatIsPrivate;
1133
1223
  /** Tell the user once (per daemon run) how to enable Threaded Mode. */
1134
1224
  private notifyThreadedFallback;
@@ -1141,7 +1231,19 @@ export declare class TelegramNotificationDaemon {
1141
1231
  */
1142
1232
  private startOwnershipHeartbeatTimer;
1143
1233
  private stopOwnershipHeartbeatTimer;
1144
- /** Run a root scan, guarding against overlapping scans from the timer + loop. */
1234
+ /**
1235
+ * Run a root scan, guarding against overlapping scans from the timer + loop.
1236
+ *
1237
+ * A reconciliation pass is retried every scan interval, so a failed one must
1238
+ * never tear the owner down. Both callers are fatal boundaries: the timer
1239
+ * fires and forgets, and the run loop awaits without a handler. A rejection
1240
+ * escaping either one (a shared topic authority whose lock or compare-and-set
1241
+ * is momentarily unavailable is the observed case) reaches the process-level
1242
+ * handler, which exits the daemon: every session topic is then left as an
1243
+ * unarchived shell that answers nothing, and the sessions that were still
1244
+ * live lose notifications too. Report the failure and let the next scan
1245
+ * retry instead.
1246
+ */
1145
1247
  private runScan;
1146
1248
  private startScanTimer;
1147
1249
  private stopScanTimer;
@@ -51,16 +51,25 @@ export interface CallbackRoute {
51
51
  sessionId: string;
52
52
  actionId: string;
53
53
  answer: TelegramCallbackAnswer;
54
+ /** Durable audit metadata only; never sufficient to route a callback after restart. */
55
+ chatId?: string;
56
+ messageId?: number;
57
+ ownerId?: string;
58
+ generation?: number;
54
59
  }
55
60
  export interface SerializedAliasTable {
56
- version: 1;
61
+ version: 1 | 2;
57
62
  next: number;
58
63
  routes: Record<string, CallbackRoute>;
59
64
  }
60
65
  export interface AliasTable {
66
+ allocate(isReserved?: (alias: string) => boolean): string;
67
+ activate(alias: string, route: CallbackRoute): boolean;
61
68
  put(route: CallbackRoute): string;
62
69
  get(alias: string): CallbackRoute | undefined;
70
+ update(alias: string, patch: Partial<CallbackRoute>): boolean;
63
71
  delete(alias: string): boolean;
72
+ clear(): void;
64
73
  serialize(): SerializedAliasTable;
65
74
  load(json: unknown): void;
66
75
  entries(): Array<[string, CallbackRoute]>;
@@ -130,6 +139,11 @@ export declare function readEndpoint(path: string): {
130
139
  pid?: number;
131
140
  stale?: boolean;
132
141
  };
142
+ /** A bounded, diagnostic-only notification mirrored to reference-client embedders. */
143
+ export type TelegramReferenceDiagnostic = Readonly<{
144
+ code: "action_unavailable";
145
+ requiredCapabilities: readonly string[];
146
+ }>;
133
147
  /** Options for {@link runTelegramReferenceClient}. */
134
148
  export interface TelegramReferenceOptions {
135
149
  botToken: string;
@@ -138,6 +152,11 @@ export interface TelegramReferenceOptions {
138
152
  apiBase?: string;
139
153
  fetchImpl?: typeof fetch;
140
154
  sound?: TelegramNotificationSound;
155
+ /**
156
+ * Additive mirror of bounded logger diagnostics. It never receives tokens,
157
+ * identifiers, raw frames, Telegram content, or WebSocket replies.
158
+ */
159
+ onDiagnostic?: (diagnostic: TelegramReferenceDiagnostic) => void;
141
160
  }
142
161
  /**
143
162
  * Run the reference bridge until the WebSocket closes. Sends `action_needed` to