super-dsh 0.1.0 → 0.1.2

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 (178) hide show
  1. package/agent-agy/bridge/agy_bridge.py +250 -0
  2. package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
  3. package/agent-agy/cordis.patch.yml +49 -0
  4. package/agent-agy/dist/adapter.d.ts +15 -0
  5. package/agent-agy/dist/adapter.js +42 -0
  6. package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
  7. package/agent-agy/dist/agent-preset-agy.js +169 -0
  8. package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
  9. package/agent-agy/dist/agent-preset-projection.js +33 -0
  10. package/agent-agy/dist/agent.d.ts +137 -0
  11. package/agent-agy/dist/agent.js +493 -0
  12. package/agent-agy/dist/agy-cli-client.d.ts +137 -0
  13. package/agent-agy/dist/agy-cli-client.js +301 -0
  14. package/agent-agy/dist/agy-client.d.ts +160 -0
  15. package/agent-agy/dist/agy-client.js +252 -0
  16. package/agent-agy/dist/agy-sessions.d.ts +24 -0
  17. package/agent-agy/dist/agy-sessions.js +116 -0
  18. package/agent-agy/dist/agy-store.d.ts +39 -0
  19. package/agent-agy/dist/agy-store.js +83 -0
  20. package/agent-agy/dist/inbox.d.ts +26 -0
  21. package/agent-agy/dist/inbox.js +53 -0
  22. package/agent-agy/dist/index.d.ts +80 -0
  23. package/agent-agy/dist/index.js +505 -0
  24. package/agent-agy/dist/models.d.ts +41 -0
  25. package/agent-agy/dist/models.js +74 -0
  26. package/agent-agy/dist/world-plugin.d.ts +9 -0
  27. package/agent-agy/dist/world-plugin.js +97 -0
  28. package/agent-agy/package.json +64 -0
  29. package/agent-claude/cordis.patch.yml +42 -0
  30. package/agent-claude/dist/adapter.js +62 -0
  31. package/agent-claude/dist/agent-preset-claude.js +169 -0
  32. package/agent-claude/dist/agent-preset-projection.js +30 -0
  33. package/agent-claude/dist/agent.js +1271 -0
  34. package/agent-claude/dist/claude-client.js +357 -0
  35. package/agent-claude/dist/claude-events.js +223 -0
  36. package/agent-claude/dist/claude-home.js +29 -0
  37. package/agent-claude/dist/commands.js +182 -0
  38. package/agent-claude/dist/content.js +134 -0
  39. package/agent-claude/dist/inbox.js +53 -0
  40. package/agent-claude/dist/index.js +555 -0
  41. package/agent-claude/dist/input-queue.js +64 -0
  42. package/agent-claude/dist/interaction.js +271 -0
  43. package/agent-claude/dist/models.js +158 -0
  44. package/agent-claude/dist/permission.js +120 -0
  45. package/agent-claude/dist/session-id.js +20 -0
  46. package/agent-claude/dist/session-map.js +165 -0
  47. package/agent-claude/dist/world-plugin.js +14 -0
  48. package/agent-claude/package.json +74 -0
  49. package/agent-codex/README.md +74 -0
  50. package/agent-codex/cordis.patch.yml +77 -0
  51. package/agent-codex/dist/adapter.js +55 -0
  52. package/agent-codex/dist/agent-preset-codex.js +171 -0
  53. package/agent-codex/dist/agent-preset-projection.js +37 -0
  54. package/agent-codex/dist/agent.js +1056 -0
  55. package/agent-codex/dist/app-home.js +65 -0
  56. package/agent-codex/dist/codex-client.js +469 -0
  57. package/agent-codex/dist/codex-events.js +297 -0
  58. package/agent-codex/dist/codex-store.js +190 -0
  59. package/agent-codex/dist/inbox.js +53 -0
  60. package/agent-codex/dist/index.js +605 -0
  61. package/agent-codex/dist/models.js +152 -0
  62. package/agent-codex/dist/permission.js +71 -0
  63. package/agent-codex/dist/session-map.js +170 -0
  64. package/agent-codex/dist/spike.js +46 -0
  65. package/agent-codex/dist/world-plugin.js +96 -0
  66. package/agent-codex/package.json +78 -0
  67. package/agent-hermes/README.md +144 -0
  68. package/agent-hermes/cordis.patch.yml +83 -0
  69. package/agent-hermes/dist/adapter.js +83 -0
  70. package/agent-hermes/dist/agent-preset-hermes.js +171 -0
  71. package/agent-hermes/dist/agent-preset-projection.js +34 -0
  72. package/agent-hermes/dist/agent.js +1068 -0
  73. package/agent-hermes/dist/hermes-client.js +953 -0
  74. package/agent-hermes/dist/hermes-events.js +289 -0
  75. package/agent-hermes/dist/hermes-store.js +158 -0
  76. package/agent-hermes/dist/inbox.js +53 -0
  77. package/agent-hermes/dist/index.js +541 -0
  78. package/agent-hermes/dist/models.js +324 -0
  79. package/agent-hermes/dist/permission.js +60 -0
  80. package/agent-hermes/dist/world-plugin.js +101 -0
  81. package/agent-hermes/package.json +76 -0
  82. package/agent-hub/cordis.patch.yml +12 -0
  83. package/agent-hub/dist/agent-roster.d.ts +34 -0
  84. package/agent-hub/dist/agent-roster.js +27 -0
  85. package/agent-hub/dist/carrier.d.ts +43 -0
  86. package/agent-hub/dist/carrier.js +241 -0
  87. package/agent-hub/dist/client-shim.d.ts +28 -0
  88. package/agent-hub/dist/client-shim.js +251 -0
  89. package/agent-hub/dist/envelope.d.ts +54 -0
  90. package/agent-hub/dist/envelope.js +92 -0
  91. package/agent-hub/dist/gateway.d.ts +28 -0
  92. package/agent-hub/dist/gateway.js +18 -0
  93. package/agent-hub/dist/index-pass.d.ts +22 -0
  94. package/agent-hub/dist/index-pass.js +47 -0
  95. package/agent-hub/dist/index.d.ts +29 -0
  96. package/agent-hub/dist/index.js +29 -0
  97. package/agent-hub/dist/labels.d.ts +45 -0
  98. package/agent-hub/dist/labels.js +83 -0
  99. package/agent-hub/dist/ownership.d.ts +37 -0
  100. package/agent-hub/dist/ownership.js +54 -0
  101. package/agent-hub/dist/roster.d.ts +28 -0
  102. package/agent-hub/dist/roster.js +38 -0
  103. package/agent-hub/dist/rpc.d.ts +38 -0
  104. package/agent-hub/dist/rpc.js +52 -0
  105. package/agent-hub/dist/spawn-world.d.ts +46 -0
  106. package/agent-hub/dist/spawn-world.js +72 -0
  107. package/agent-hub/dist/targets.d.ts +33 -0
  108. package/agent-hub/dist/targets.js +30 -0
  109. package/agent-hub/dist/world-entry.d.ts +31 -0
  110. package/agent-hub/dist/world-entry.js +51 -0
  111. package/agent-hub/dist/world-host.d.ts +69 -0
  112. package/agent-hub/dist/world-host.js +44 -0
  113. package/agent-hub/dist/world-join.d.ts +16 -0
  114. package/agent-hub/dist/world-join.js +122 -0
  115. package/agent-hub/dist/world-mount.d.ts +52 -0
  116. package/agent-hub/dist/world-mount.js +52 -0
  117. package/agent-hub/dist/world-mux.d.ts +72 -0
  118. package/agent-hub/dist/world-mux.js +224 -0
  119. package/agent-hub/dist/world-provision.d.ts +20 -0
  120. package/agent-hub/dist/world-provision.js +122 -0
  121. package/agent-hub/dist/world-web-server.d.ts +162 -0
  122. package/agent-hub/dist/world-web-server.js +248 -0
  123. package/agent-hub/lib/client/index.js +242 -0
  124. package/agent-hub/package.json +65 -0
  125. package/agent-omp/README.md +77 -0
  126. package/agent-omp/cordis.patch.yml +92 -0
  127. package/agent-omp/dist/adapter.js +36 -0
  128. package/agent-omp/dist/agent-preset-omp.js +171 -0
  129. package/agent-omp/dist/agent-preset-projection.js +36 -0
  130. package/agent-omp/dist/agent.js +1145 -0
  131. package/agent-omp/dist/app-home.js +86 -0
  132. package/agent-omp/dist/discovery.js +116 -0
  133. package/agent-omp/dist/inbox.js +53 -0
  134. package/agent-omp/dist/index.js +574 -0
  135. package/agent-omp/dist/knobs.js +50 -0
  136. package/agent-omp/dist/lazy-rpc.js +161 -0
  137. package/agent-omp/dist/mobile/gesture.js +148 -0
  138. package/agent-omp/dist/mobile/zoom-guard.js +294 -0
  139. package/agent-omp/dist/mobile-boot.js +38 -0
  140. package/agent-omp/dist/models.js +299 -0
  141. package/agent-omp/dist/omp-cli.js +147 -0
  142. package/agent-omp/dist/omp-disk-discovery.js +178 -0
  143. package/agent-omp/dist/omp-store.js +518 -0
  144. package/agent-omp/dist/pairing.js +45 -0
  145. package/agent-omp/dist/permission.js +96 -0
  146. package/agent-omp/dist/protocol.js +13 -0
  147. package/agent-omp/dist/rpc-types.js +1 -0
  148. package/agent-omp/dist/sdk-client.js +378 -0
  149. package/agent-omp/dist/sidecar-client.js +195 -0
  150. package/agent-omp/dist/store/adapter.js +47 -0
  151. package/agent-omp/dist/store/db.js +210 -0
  152. package/agent-omp/dist/store/index.js +59 -0
  153. package/agent-omp/dist/store/reconcile.js +51 -0
  154. package/agent-omp/dist/store/schema.js +40 -0
  155. package/agent-omp/dist/world-plugin.js +96 -0
  156. package/agent-omp/package.json +97 -0
  157. package/agent-omp/sidecar/main.ts +481 -0
  158. package/agent-pi/README.md +67 -0
  159. package/agent-pi/cordis.patch.yml +80 -0
  160. package/agent-pi/dist/adapter.js +53 -0
  161. package/agent-pi/dist/agent-preset-pi.js +169 -0
  162. package/agent-pi/dist/agent-preset-projection.js +29 -0
  163. package/agent-pi/dist/agent.js +949 -0
  164. package/agent-pi/dist/inbox.js +53 -0
  165. package/agent-pi/dist/index.js +595 -0
  166. package/agent-pi/dist/knobs.js +24 -0
  167. package/agent-pi/dist/models.js +200 -0
  168. package/agent-pi/dist/permission.js +66 -0
  169. package/agent-pi/dist/pi-client.js +224 -0
  170. package/agent-pi/dist/pi-events.js +155 -0
  171. package/agent-pi/dist/pi-home.js +55 -0
  172. package/agent-pi/dist/safe-json.js +25 -0
  173. package/agent-pi/dist/session-map.js +119 -0
  174. package/agent-pi/dist/world-plugin.js +97 -0
  175. package/agent-pi/package.json +77 -0
  176. package/cordis.patch.yml +22 -19
  177. package/package.json +88 -25
  178. package/client.js +0 -4
@@ -0,0 +1,541 @@
1
+ /**
2
+ * agent-hermes provider — Hermes provider plugin for DeepSeek Harness
3
+ * (Agent Worlds AW-H, TUI gateway line).
4
+ *
5
+ * Replaces the built-in agent loop with an `AgentFactory` that drives a
6
+ * `HermesGatewayClient` (one spawned `python -m tui_gateway.entry` child per
7
+ * session) and bridges the projected wire stream into the Dash Agent/Session
8
+ * contracts. Mirrors `@deepseek-ai/dsh-agent-loop`'s creation transaction
9
+ * (prepare → owned storage handle → setup → publish) so the session + agent
10
+ * publish as one ordered lifecycle AND the DSH session log is written through
11
+ * the upstream `sessionPersistence` service.
12
+ *
13
+ * Session identity (2026-09-17, "mapping only"): the DSH session id is the
14
+ * authority — supplied by the harness at create (`options.sessionId`) and
15
+ * resume (`options.resumeSessionId`). The gateway's session id only
16
+ * materializes after the first prompt, so the pairing is persisted in the
17
+ * adapter's DSH-side app dir (`<home>/dsh-sessions.json`, see hermes-store.ts).
18
+ * Resume resolves that record and fails closed when the gateway session is
19
+ * unknown. Lazy materialization: `createAgent` spawns the gateway child but
20
+ * binds NO gateway session — the FIRST prompt does `ensureStarted()` +
21
+ * `createSession` (fresh) / `resumeSession` (re-attach), and
22
+ * `followGatewaySessionId` upserts the map once the session id materializes.
23
+ */
24
+ import { realpathSync, statSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { Service } from "@deepseek-ai/cordis";
27
+ import { emitAgentEvent, installModelSelection } from "@deepseek-ai/dsh-agent";
28
+ import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
29
+ import { HermesGatewayClient, HERMES_SPAWN_GRACE_MS } from "./hermes-client.js";
30
+ import { HermesAgent } from "./agent.js";
31
+ import { HermesLlmAdapter, HERMES_PROVIDER_ID } from "./adapter.js";
32
+ import { SingleHermesPresetRoster } from "./agent-preset-hermes.js";
33
+ import { hermesAgentPresetProjection } from "./agent-preset-projection.js";
34
+ import { sessionRecord, upsertSession } from "./hermes-store.js";
35
+ import { hermesProviderSlugs, readHermesModelCatalog } from "./models.js";
36
+ import { envApprovalMode, defaultPermissionPreset, presetFromEvents } from "./permission.js";
37
+ /** Realpath of a recorded cwd, accepted only when it names an existing directory. */
38
+ function validatedCwd(cwd) {
39
+ if (cwd === undefined)
40
+ return undefined;
41
+ try {
42
+ const canonical = realpathSync(cwd);
43
+ return statSync(canonical).isDirectory() ? canonical : undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ /** Diagnostic trace (HERMES_TRACE=1 on the dsh process enables stderr tracing). */
50
+ const trace = (...parts) => {
51
+ if (process.env.HERMES_TRACE === "1")
52
+ process.stderr.write(`[hermes-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
53
+ };
54
+ /** Open a read handle, pull the complete stored log, close. */
55
+ async function readStoredEvents(persistence, id, signal) {
56
+ const handle = await persistence.open(id, "read", signal === undefined ? {} : { signal });
57
+ try {
58
+ return (await handle.read(0, undefined, signal === undefined ? {} : { signal })).events;
59
+ }
60
+ finally {
61
+ await handle.close();
62
+ }
63
+ }
64
+ export class HermesProvider extends Service {
65
+ /**
66
+ * `agentDefaultModel` and `settings` are load-bearing, not decorative (OMP
67
+ * twin, same boot-order trap): the harness owns the "default for new sessions"
68
+ * selection there, and this bridge is the only thing that feeds it the hermes
69
+ * config's real default. Without the inject the context cannot resolve the
70
+ * service at apply time and the push in #registerDefaultModel never fires.
71
+ */
72
+ static inject = ["agents", "sessions", "llm", "agentDefaultModel", "settings"];
73
+ /** Plain holder — prevents Cordis re-tracing the factory's ctx through a caller shadow. */
74
+ runtime;
75
+ /**
76
+ * The world's DSH home — captured once from the world's own `dshHomePath`
77
+ * (never ambient env inside plugins). It anchors adapter-owned DSH state
78
+ * (the session map) only; the hermes runtime keeps its native `~/.hermes`
79
+ * home (2026-09-17 ruling).
80
+ */
81
+ home;
82
+ /** Echo/staleness guard for the app-wide default-model push. */
83
+ defaultModelKey = "";
84
+ /** Memoized async catalog (probe-client-then-close, resolved once per process). */
85
+ catalogPromise;
86
+ constructor(ctx) {
87
+ super(ctx, "hermesProvider");
88
+ this.runtime = { ctx };
89
+ // Native-home ruling (2026-09-17): the Hermes runtime keeps its own native
90
+ // home (~/.hermes), never redirected. this.home is only the world's DSH
91
+ // home, anchoring adapter-owned DSH state (the session map).
92
+ this.home = this.#resolveHome();
93
+ trace(`home: worldHome=${this.home}`);
94
+ ctx.effect(() => ctx.agents.setFactory(this), "hermesProvider.setFactory()");
95
+ this.#registerModelCatalog();
96
+ this.#registerDefaultModel();
97
+ // The roster must be visible to the API gateway's root-level remote
98
+ // enumeration; register it on this plugin's own (top-level) fiber.
99
+ new SingleHermesPresetRoster(ctx);
100
+ // Drive the `agentPreset` session projection ourselves.
101
+ ctx.inject(["sessionProjections"], (scoped) => {
102
+ scoped.sessionProjections.register(hermesAgentPresetProjection);
103
+ });
104
+ }
105
+ /**
106
+ * The factory owns its sessions' route (user directive 2026-09-15,
107
+ * restated 2026-09-17): the ambient `agentDefaultModel` selection is NOT
108
+ * authoritative here — a session created by this factory always runs on the
109
+ * gateway's real default route {provider: slug, model: id} unless the caller
110
+ * asked for a model this factory serves (a gateway provider slug, or the
111
+ * legacy `hermes` placeholder). The pair is canonicalized through the
112
+ * catalog entry (the r4 dedupe gives each id ONE provider route, so the
113
+ * pinned route always resolves), and ids stay the VERBATIM gateway
114
+ * selection strings. Unavailable catalog → no route fields (the placeholder
115
+ * never leaks — the hermes config default applies).
116
+ */
117
+ async sessionAgentOptions(requested) {
118
+ const catalog = await this.catalog();
119
+ const requestedProvider = requested?.provider;
120
+ const requestedModel = typeof requested?.model === "string" ? requested.model : undefined;
121
+ const providerServed = requestedProvider === HERMES_PROVIDER_ID ||
122
+ (requestedProvider !== undefined && catalog.models.some((entry) => entry.provider === requestedProvider));
123
+ const explicit = providerServed && requestedModel !== undefined && requestedModel !== "" ? requestedModel : undefined;
124
+ const model = explicit ?? catalog.defaultModel;
125
+ if (model === undefined)
126
+ return { ...(requested ?? {}) };
127
+ // Canonical route: the model's catalog entry pins the real provider slug.
128
+ const entry = catalog.models.find((candidate) => candidate.id === model);
129
+ const provider = entry?.provider ??
130
+ (explicit && requestedProvider !== undefined && requestedProvider !== HERMES_PROVIDER_ID
131
+ ? requestedProvider // advisory catalog: pass the served slug through unlisted
132
+ : catalog.provider);
133
+ return { ...(requested ?? {}), provider, model };
134
+ }
135
+ /** The memoized async catalog (probe-client-then-close, resolved once). */
136
+ catalog() {
137
+ if (this.catalogPromise === undefined) {
138
+ this.catalogPromise = this.#probeCatalog();
139
+ }
140
+ return this.catalogPromise;
141
+ }
142
+ /**
143
+ * Probe the gateway `model.options` once and close the probe client. The
144
+ * probe client is the ONLY spawn that arms the idle-probe reaper (explicit
145
+ * spawnGraceMs): it never materializes a session on purpose, so the grace
146
+ * bounds it. Agent-held clients (createAgent/resume) never pass it.
147
+ */
148
+ async #probeCatalog() {
149
+ const client = HermesGatewayClient.spawn({ spawnGraceMs: HERMES_SPAWN_GRACE_MS });
150
+ try {
151
+ return await readHermesModelCatalog(client);
152
+ }
153
+ finally {
154
+ client.close();
155
+ }
156
+ }
157
+ /**
158
+ * The write path is mandatory now (2026-09-16): this factory owns the DSH
159
+ * session log through the upstream persistence service.
160
+ */
161
+ requirePersistence(op) {
162
+ const persistence = this.runtime.ctx.get("sessionPersistence");
163
+ if (persistence === undefined) {
164
+ throw new Error(`cannot ${op} a Hermes session: session persistence is not configured`);
165
+ }
166
+ return persistence;
167
+ }
168
+ /** Take a fresh session's write ownership (nothing is appended here). */
169
+ async createStoredSession(persistence, session, signal) {
170
+ const handle = await persistence.create(session.header, {
171
+ inheritedEventCount: session.inheritedEventCount,
172
+ ...(signal === undefined ? {} : { signal }),
173
+ });
174
+ return { handle, storedCount: 0 };
175
+ }
176
+ /**
177
+ * Runtime facts the agent stamps into the log: Hermes's own system prompt
178
+ * (the settled `session.info` payload's `system_prompt`, captured by the
179
+ * client once non-empty) and the effective route metadata (selected hermes
180
+ * model, else the catalog default, plus the catalog's context window).
181
+ */
182
+ agentRuntime(client, cwd, resumeStoredSessionId = null) {
183
+ return {
184
+ systemPrompt: () => {
185
+ const prompt = client.systemPrompt;
186
+ return prompt === "" ? undefined : prompt;
187
+ },
188
+ sessionTitle: () => {
189
+ const title = client.title;
190
+ return title === "" ? undefined : title;
191
+ },
192
+ routeContext: async (selection) => {
193
+ const catalog = await this.catalog();
194
+ const model = selection?.model !== undefined && selection.model !== "" ? selection.model : catalog.defaultModel;
195
+ if (model === undefined)
196
+ return undefined;
197
+ // Canonical route: the entry pins the real provider slug (r4 dedupe
198
+ // gives each id exactly one). A provider-matched find honors an
199
+ // explicit selection; the id-only fallback covers legacy/stale pairs.
200
+ const entry = catalog.models.find((candidate) => candidate.id === model && (selection?.provider === undefined || candidate.provider === selection.provider)) ?? catalog.models.find((candidate) => candidate.id === model);
201
+ return {
202
+ provider: entry?.provider ?? selection?.provider ?? catalog.provider,
203
+ model,
204
+ ...(entry?.contextWindow !== undefined && entry.contextWindow > 0 ? { contextWindow: entry.contextWindow } : {}),
205
+ };
206
+ },
207
+ session: { cwd, resumeStoredSessionId },
208
+ };
209
+ }
210
+ /** The world's DSH home: boot-provided dshHomePath first, env last. */
211
+ #resolveHome() {
212
+ const fromBoot = this.runtime.ctx.get("dshHomePath");
213
+ if (typeof fromBoot === "string" && fromBoot !== "")
214
+ return fromBoot;
215
+ if (typeof fromBoot === "function") {
216
+ const resolved = fromBoot();
217
+ if (typeof resolved === "string" && resolved !== "")
218
+ return resolved;
219
+ }
220
+ return process.env.DSH_HOME ?? join(process.cwd(), ".tests", "aw");
221
+ }
222
+ /**
223
+ * Push the gateway's default model into the world's `agentDefaultModel`
224
+ * (one-directional on purpose). The catalog is async, so the push resolves
225
+ * through the memoized promise; `defaultModel === undefined` (unavailable
226
+ * catalog) skips — the placeholder never leaks outward.
227
+ */
228
+ #registerDefaultModel() {
229
+ this.runtime.ctx.inject(["agentDefaultModel", "settings"], (mCtx) => {
230
+ const service = mCtx.get("agentDefaultModel");
231
+ if (service === undefined)
232
+ return;
233
+ void this.catalog()
234
+ .then((catalog) => {
235
+ if (catalog.defaultModel === undefined) {
236
+ trace("default model push skipped: catalog default unavailable");
237
+ return;
238
+ }
239
+ // Real-slug route (2026-09-17 parity ruling): the default model's
240
+ // canonical catalog entry pins the gateway provider slug; the
241
+ // top-level `provider` string is the fallback when the id is
242
+ // somehow unlisted. Ordering note: #registerModelCatalog ran first
243
+ // at boot, so its handle.replace (placeholder → real slugs) on the SAME memoized
244
+ // promise has already swapped the routes in before this push fires.
245
+ const entry = catalog.models.find((candidate) => candidate.id === catalog.defaultModel);
246
+ const target = { provider: entry?.provider ?? catalog.provider, model: catalog.defaultModel };
247
+ const targetKey = `${target.provider}/${target.model}`;
248
+ const current = service.currentSelection?.();
249
+ const currentKey = current === undefined ? undefined : `${current.provider}/${current.model}`;
250
+ if (currentKey === targetKey || currentKey === this.defaultModelKey)
251
+ return;
252
+ trace(`default model: ${currentKey ?? "none"} → ${targetKey}`);
253
+ const settings = mCtx.get("settings");
254
+ void Promise.resolve(settings?.replace?.("agent-default-model", target))
255
+ .then(() => service.saveSelection?.(target))
256
+ .then(() => {
257
+ this.defaultModelKey = targetKey;
258
+ this.runtime.ctx.logger.info(`hermes-provider: default model ← hermes catalog: ${targetKey}`);
259
+ })
260
+ .catch((error) => {
261
+ this.runtime.ctx.logger.warn(`hermes-provider: default model save failed: ${String(error)}`);
262
+ });
263
+ })
264
+ .catch((error) => {
265
+ this.runtime.ctx.logger.warn(`hermes-provider: catalog probe failed: ${String(error)}`);
266
+ });
267
+ });
268
+ }
269
+ /**
270
+ * Register the Hermes-backed LLM adapter: the single placeholder route
271
+ * `hermes` at boot (the selector is alive while the async probe runs), then
272
+ * an ATOMIC swap to the distinct real provider slugs once the first catalog
273
+ * probe resolves (`AdapterRegistrationHandle.replace` — validated in full,
274
+ * one synchronous section, no observable gap). 2026-09-17 parity ruling.
275
+ */
276
+ #registerModelCatalog() {
277
+ const llm = this.runtime.ctx.get("llm");
278
+ if (llm === undefined)
279
+ return;
280
+ // The adapter resolves the async catalog through the memoized supplier.
281
+ const handle = llm.registerAdapter([HERMES_PROVIDER_ID], new HermesLlmAdapter(() => this.catalog()));
282
+ // Registered BEFORE #registerDefaultModel in the constructor, so this
283
+ // .then runs before the default-model push on the same memoized promise:
284
+ // the pushed {provider, model} route always resolves by the time it lands.
285
+ void this.catalog()
286
+ .then((catalog) => {
287
+ const slugs = hermesProviderSlugs(catalog);
288
+ if (slugs.length === 0) {
289
+ this.runtime.ctx.logger.warn("hermes-provider: catalog probe empty — the placeholder hermes route stays");
290
+ return;
291
+ }
292
+ handle.replace(slugs);
293
+ this.runtime.ctx.logger.info(`hermes-provider: model routes ← ${slugs.length} gateway provider groups (${slugs.join(", ")})`);
294
+ })
295
+ .catch((error) => {
296
+ this.runtime.ctx.logger.warn(`hermes-provider: model route swap failed: ${String(error)}`);
297
+ });
298
+ }
299
+ async createAgent(ownerCtx, options) {
300
+ const loopCtx = this.runtime.ctx;
301
+ const id = options.sessionId;
302
+ const meta = options.meta ?? {};
303
+ const cwd = meta.cwd;
304
+ // Fork: Hermes has no native fork (session.branch is a P2 gap).
305
+ if (meta.parentSession !== undefined) {
306
+ throw new Error(`cannot fork session "${meta.parentSession}" onto the Hermes provider: Hermes has no native session fork`);
307
+ }
308
+ const persistence = this.requirePersistence("create");
309
+ // The preset is record-only (no launch flag — approval is runtime).
310
+ const preset = envApprovalMode() ?? defaultPermissionPreset(loopCtx);
311
+ const spawnCwd = validatedCwd(cwd) ?? process.cwd();
312
+ trace(`create id=${id} preset=${preset ?? "none"} spawnCwd=${spawnCwd}${envApprovalMode() === undefined ? "" : " (env override)"}`);
313
+ // Lazy session materialization (ruling 1): spawn the gateway child (no
314
+ // session bound), record the pairing with gatewaySessionId:null. The FIRST
315
+ // prompt materializes the gateway session via createSession.
316
+ // Agent-held client: the spawn-grace reaper is NEVER armed here (2026-09-17
317
+ // ruling 4 — the session may legitimately materialize far beyond any
318
+ // grace; disposal is the agent lifecycle's job, not a timer's).
319
+ const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
320
+ const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
321
+ ...(options.seed === undefined ? {} : { seed: options.seed }),
322
+ ...(meta === undefined ? {} : { meta }),
323
+ ...(options.inheritedEventCount === undefined ? {} : { inheritedEventCount: options.inheritedEventCount }),
324
+ }));
325
+ let stored;
326
+ let handedOff = false;
327
+ try {
328
+ stored = await this.createStoredSession(persistence, preparation.session, options.signal);
329
+ upsertSession(this.home, String(id), {
330
+ gatewaySessionId: null,
331
+ cwd: spawnCwd,
332
+ createdAt: Date.now(),
333
+ preset: preset ?? null,
334
+ });
335
+ this.followGatewaySessionId(id, client);
336
+ handedOff = true;
337
+ const agentOptions = await this.sessionAgentOptions(options.agentOptions);
338
+ return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "startup", { enterSession: true, preparation }, stored, this.agentRuntime(client, spawnCwd));
339
+ }
340
+ catch (error) {
341
+ trace(`create ${id} THREW: ${String(error)}`);
342
+ client.close();
343
+ if (!handedOff)
344
+ await stored?.handle.close().catch(() => { });
345
+ throw error;
346
+ }
347
+ finally {
348
+ if (!handedOff)
349
+ preparation[Symbol.dispose]();
350
+ }
351
+ }
352
+ async resume(ownerCtx, options) {
353
+ const loopCtx = this.runtime.ctx;
354
+ const id = options.resumeSessionId;
355
+ // Identity (2026-09-17 "mapping only"): the DSH session id resolves through
356
+ // the adapter-owned map. A missing record (or a never-materialized gateway
357
+ // session) is a genuine unknown: fail closed with the DSH id in the message.
358
+ const record = sessionRecord(this.home, String(id));
359
+ trace(`resume id=${id} record=${record === undefined ? "MISSING" : record.gatewayStoredSessionId ?? "NO-STORED-ID"}`);
360
+ if (record === undefined) {
361
+ throw new Error(`cannot resume session "${id}": no Hermes gateway session is recorded for this Dash session id`);
362
+ }
363
+ if (record.gatewayStoredSessionId === null || record.gatewayStoredSessionId === undefined) {
364
+ throw new Error(`cannot resume session "${id}": no durable gateway session id is recorded (this record predates durable-id capture; re-create the session)`);
365
+ }
366
+ // Spawn cwd comes from the recorded pairing — re-realpath and re-verify.
367
+ const spawnCwd = validatedCwd(record.cwd);
368
+ if (spawnCwd === undefined) {
369
+ throw new Error(`cannot resume session "${id}": its recorded working directory no longer exists`);
370
+ }
371
+ const persistence = this.requirePersistence("resume");
372
+ // Preset (record-only): env override, else the recorded preset, else the
373
+ // persisted session log, else the permission service's default.
374
+ const envMode = envApprovalMode();
375
+ const preset = envMode !== undefined
376
+ ? envMode
377
+ : record.preset ??
378
+ presetFromEvents(await readStoredEvents(persistence, id, options.signal)) ??
379
+ defaultPermissionPreset(loopCtx);
380
+ trace(`resume id=${id} spawnCwd=${spawnCwd} preset=${preset ?? "none"}`);
381
+ // Spawn the gateway child (no session bound); the FIRST prompt re-attaches
382
+ // via resumeSession(record.gatewayStoredSessionId) — the DURABLE key.
383
+ const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
384
+ let handle;
385
+ let stored;
386
+ let preparation;
387
+ let handedOff = false;
388
+ try {
389
+ handle = await persistence.open(id, "write", options.signal === undefined ? {} : { signal: options.signal });
390
+ const cold = await handle.read(0, undefined, options.signal === undefined ? {} : { signal: options.signal });
391
+ const closers = interruptedTurnClosers(cold.events);
392
+ if (closers.length > 0)
393
+ await handle.append(closers);
394
+ preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
395
+ seed: [...cold.events, ...closers],
396
+ meta: structuredClone(handle.header),
397
+ inheritedEventCount: handle.inheritedEventCount,
398
+ eventState: cold.eventState,
399
+ }));
400
+ stored = { handle, storedCount: cold.events.length + closers.length };
401
+ trace(`resume id=${id} seeded ${cold.events.length} events (+${closers.length} closers)`);
402
+ handle = undefined; // ownership passes to setupAndPublish
403
+ this.followGatewaySessionId(id, client);
404
+ handedOff = true;
405
+ const agentOptions = await this.sessionAgentOptions(options.agentOptions);
406
+ return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(client, spawnCwd, record.gatewayStoredSessionId));
407
+ }
408
+ catch (error) {
409
+ trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
410
+ client.close();
411
+ if (!handedOff)
412
+ await handle?.close().catch(() => { });
413
+ throw error;
414
+ }
415
+ finally {
416
+ if (!handedOff)
417
+ preparation?.[Symbol.dispose]();
418
+ }
419
+ }
420
+ /**
421
+ * Persist the Dash↔gateway pairing at session ADOPTION (the
422
+ * session.create/resume response — the materialization moment) and tolerate
423
+ * `stored_session_id` DRIFT: the gateway may remint the durable key on
424
+ * resume, so the upsert always tracks the latest adopted pair (2026-09-17
425
+ * ruling). Arrow FIELD (not a `#` method): the provider is exposed through
426
+ * a Cordis tracing proxy.
427
+ */
428
+ followGatewaySessionId = (id, client) => {
429
+ const observe = () => {
430
+ const live = client.sessionId;
431
+ const stored = client.storedSessionId;
432
+ if (live === null && stored === null)
433
+ return;
434
+ try {
435
+ upsertSession(this.home, String(id), {
436
+ ...(live === null ? {} : { gatewaySessionId: live }),
437
+ ...(stored === null ? {} : { gatewayStoredSessionId: stored }),
438
+ });
439
+ trace(`gateway ids recorded for ${String(id)}: live=${live ?? "none"} stored=${stored ?? "none"}`);
440
+ }
441
+ catch (error) {
442
+ this.runtime.ctx.logger.warn(`hermes-provider: failed to persist the gateway ids for ${String(id)}: ${String(error)}`);
443
+ }
444
+ };
445
+ observe();
446
+ const off = client.onAdopted(() => {
447
+ observe();
448
+ off();
449
+ });
450
+ };
451
+ }
452
+ /** Durable flush of the session's pre-publication suffix. */
453
+ async function appendUnstoredSuffix(stored, session) {
454
+ const suffix = session.snapshotEvents(SessionLogOffset(stored.storedCount));
455
+ if (suffix.length > 0)
456
+ await stored.handle.append(suffix);
457
+ stored.storedCount += suffix.length;
458
+ }
459
+ /**
460
+ * Shared creation/resume transaction: build the agent over the prepared
461
+ * session, run unpublished setup, flush the unstored suffix, and publish both
462
+ * in order. Kept as a module-level function (not a private method) because
463
+ * Cordis exposes the provider through a tracing proxy.
464
+ */
465
+ async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo) {
466
+ let detachSession;
467
+ let detachAgent;
468
+ let agent;
469
+ let idleExit;
470
+ try {
471
+ agent = new HermesAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtimeInfo);
472
+ // Pin THIS session to the hermes route. The route resolves through the
473
+ // async catalog (the memoized promise), so this is awaited before the first
474
+ // prompt — `selectForNextRequest` is the same per-session mechanism
475
+ // `session/selectModel` uses.
476
+ try {
477
+ const route = await runtimeInfo?.routeContext({ provider: agentOptions.provider, model: agentOptions.model });
478
+ if (route !== undefined) {
479
+ const selection = { provider: route.provider, model: route.model };
480
+ let picked = selection;
481
+ installModelSelection(agent.ctx, {
482
+ get current() { return picked; },
483
+ set current(next) { picked = next; },
484
+ assembled: undefined,
485
+ });
486
+ session.append("model/selection", selection);
487
+ trace(`session route pinned: ${route.provider}/${route.model}`);
488
+ }
489
+ }
490
+ catch (error) {
491
+ // Fail-soft: the boot-time default-model push remains the fallback.
492
+ trace(`session route pin failed: ${String(error)}`);
493
+ }
494
+ const commit = await setup?.(agent.ctx, agent);
495
+ commit?.commit();
496
+ await appendUnstoredSuffix(stored, session);
497
+ if (opts.enterSession) {
498
+ detachSession = agent.ctx.sessions.enter(session);
499
+ agent.ctx.sessions.announce(session);
500
+ }
501
+ detachAgent = loopCtx.agents.enter(agent, parentAgent);
502
+ loopCtx.agents.announce(agent);
503
+ emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
504
+ let disposed = false;
505
+ let unfollowOwner;
506
+ const dispose = async () => {
507
+ if (disposed)
508
+ return;
509
+ disposed = true;
510
+ trace(`dispose() called for agent ${id} (source=${source})`);
511
+ unfollowOwner?.();
512
+ await agent?.dispose();
513
+ detachAgent?.();
514
+ detachSession?.();
515
+ await stored.handle.close();
516
+ };
517
+ idleExit = () => {
518
+ void dispose().catch((error) => {
519
+ loopCtx.logger.warn(`hermes-provider: teardown of ${id} failed: ${String(error)}`);
520
+ });
521
+ };
522
+ unfollowOwner = ownerCtx.effect(() => () => {
523
+ void dispose().catch((error) => {
524
+ loopCtx.logger.warn(`hermes-provider: owner-unload teardown of ${id} failed: ${String(error)}`);
525
+ });
526
+ }, `hermesProvider.lifecycle(${id})`);
527
+ return { agent, dispose };
528
+ }
529
+ catch (error) {
530
+ trace(`setupAndPublish ${id} FAILED: ${String(error)}`);
531
+ detachAgent?.();
532
+ detachSession?.();
533
+ void agent?.dispose().catch(() => { });
534
+ client.close();
535
+ throw error;
536
+ }
537
+ finally {
538
+ opts.preparation?.[Symbol.dispose]();
539
+ }
540
+ }
541
+ export default HermesProvider;