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,289 @@
1
+ /**
2
+ * Hermes TUI-gateway event → omp wire vocabulary projection (AW-H Task 2).
3
+ *
4
+ * The wire vocabulary is the same internal `Wire*` vocabulary as
5
+ * agent-codex/src/codex-events.ts (ported from agent-omp/src/rpc-types.ts);
6
+ * the omp event `type` STRINGS are kept verbatim (`agent_start` / `turn_start`
7
+ * / `message_start` / `message_update` / `message_end` /
8
+ * `tool_execution_start` / `tool_execution_end` / `turn_end` / `agent_end`)
9
+ * so the Task 7 agent.ts port consumes the exact same switch it consumes on
10
+ * the codex line.
11
+ *
12
+ * The projector is a PURE function `projectGatewayEvent(type, payload, ctx)`:
13
+ * no I/O, no timers, no module state. Accumulation (open assistant text,
14
+ * thinking buffer, seen tool ids) is the CLIENT's job — the read-only
15
+ * `ProjectionCtx { turnOpen, openAssistant }` is the client's per-frame
16
+ * answer to the only two questions the projection needs state for.
17
+ *
18
+ * Projection mapping (as-built; grounded in the Task 1 live fixture
19
+ * `agent-hermes/spike-events@1` + gateway source pins):
20
+ *
21
+ * message.start (no payload key) → turn open: [agent_start, turn_start,
22
+ * message_start(assistant, [])]; turn
23
+ * already open: message_start alone.
24
+ * Hermes has NO thread.started /
25
+ * turn.started events — message.start
26
+ * IS the live-pinned turn-open bracket
27
+ * (Task 1 §9.2 ①), gated on
28
+ * !ctx.turnOpen so repeats stay bare.
29
+ * message.delta {text} → message_update(assistantMessageEvent
30
+ * { type:"text_delta", contentIndex:0,
31
+ * delta }) — healed with a leading
32
+ * message_start when !ctx.openAssistant.
33
+ * thinking.delta {text} → null (incl. empty-string frames —
34
+ * reasoning.delta {text, verbose?} → live display is the client's
35
+ * accumulation; the durable thinking
36
+ * block rides message.complete.reasoning)
37
+ * tool.start {tool_id, name, → tool_execution_start {toolCallId,
38
+ * context, args?, args_text?} toolName, args, argumentsJson }
39
+ * (id field is `tool_id` — Task 1 §3④
40
+ * correction; args_text, when present,
41
+ * is the RAW JSON string and wins)
42
+ * tool.complete {tool_id, name, → tool_execution_end {toolCallId,
43
+ * args, result, duration_s?, toolName, result:{content,isError},
44
+ * summary?, inline_diff?, …} isError, duration_s? } — result is a
45
+ * parsed-JSON value or raw string;
46
+ * objects render as pretty JSON text,
47
+ * strings verbatim, summary is the
48
+ * fallback. isError is ALWAYS false in
49
+ * V1: the gateway pins no error/status
50
+ * field on tool.complete.
51
+ * message.complete {text, usage, → [message_end, turn_end, agent_end]
52
+ * status:"complete", reasoning?} — message_end carries the text block
53
+ * (+ trailing thinking block from
54
+ * `reasoning`) and the RAW hermes usage
55
+ * verbatim at `usage` (brief table);
56
+ * turn_end carries the same usage at
57
+ * `data.usage` (the agent.ts seam —
58
+ * codex parity) plus `data.status`;
59
+ * agent_end closes the run.
60
+ * message.complete {status:"error", → same terminal triple, with
61
+ * error, recoverable, partial?, …} message_end {stopReason:"error",
62
+ * errorMessage, content from `partial`}
63
+ * (failure classification stays in
64
+ * agent.ts's wireFailure, unchanged)
65
+ * message.complete → same triple, plain message_end with
66
+ * {status:"interrupted", …} the partial text; the interrupt
67
+ * intent itself reaches the client via
68
+ * its own cancel path, not the wire.
69
+ * error {message} → agent_end (data.error.message),
70
+ * ONLY while ctx.turnOpen — abnormal
71
+ * finish with no message.complete.
72
+ * session.info (running true/false), } null — log-only. The settled pair
73
+ * session.usage, status.update, } message.complete →
74
+ * approval.request, } session.info(running=false) is the
75
+ * sessions.changed, } CLIENT's turn-boundary signal
76
+ * gateway.ready, message.interim, } (Task 1 §9.2), never wire.
77
+ * session.title {session_id, title} → session_title {name} (pi wire
78
+ * parity) — the AGENT mirrors it as
79
+ * `session/title` (provider source);
80
+ * empty/missing titles stay null.
81
+ * todo.updated {todos, revision} → todo_updated {todos: DSH-shaped
82
+ * [{content, status}]} — the AGENT
83
+ * appends the log-only `todo/write`
84
+ * whole-list snapshot. Items map:
85
+ * trimmed non-empty content, status
86
+ * ∈ {pending, in_progress, completed};
87
+ * `cancelled`/unknown-status and
88
+ * duplicate-content items are dropped
89
+ * (DSH's todo/write invariant has no
90
+ * cancelled status and forbids
91
+ * repeated content). A well-formed
92
+ * empty list passes through — a real
93
+ * clear (revision ≥ 1 per gateway).
94
+ * anything unrecognized }
95
+ *
96
+ * KNOWN SEAM (documented for Task 3): gateway tool-frame emission is
97
+ * CONDITIONAL and the gates differ between start and complete (server.py:
98
+ * _on_tool_start requires progress-enabled ∥ lifecycle-required-for-ui;
99
+ * _on_tool_complete additionally fires for inline_diff ∥ todo tools) — so an
100
+ * orphan tool.complete without a prior tool.start is possible. The projector
101
+ * cannot dedup/pair without state (purity contract); the CLIENT, which owns
102
+ * accumulation state, should synthesize the missing tool_execution_start for
103
+ * an unseen toolCallId before forwarding the end.
104
+ *
105
+ * Never throws: any internal failure collapses to `null` (fail-soft).
106
+ */
107
+ function asRecord(value) {
108
+ if (value === null || typeof value !== "object" || Array.isArray(value))
109
+ return null;
110
+ return value;
111
+ }
112
+ function asText(value) {
113
+ return typeof value === "string" ? value : "";
114
+ }
115
+ function assistantMessageStart() {
116
+ return { type: "message_start", message: { role: "assistant", content: [] } };
117
+ }
118
+ function textDelta(delta) {
119
+ const assistantMessageEvent = { type: "text_delta", contentIndex: 0, delta };
120
+ return { type: "message_update", assistantMessageEvent };
121
+ }
122
+ /** Render a tool.complete `result` (parsed JSON value or raw string) as the wire result text. */
123
+ function renderToolResult(payload) {
124
+ const result = payload.result;
125
+ if (typeof result === "string")
126
+ return result;
127
+ if (result !== null && result !== undefined) {
128
+ const rendered = JSON.stringify(result, null, 2);
129
+ if (typeof rendered === "string")
130
+ return rendered;
131
+ }
132
+ return asText(payload.summary);
133
+ }
134
+ /**
135
+ * message.complete — the turn-terminal event (Task 1 §9.2 ①). Emits the
136
+ * terminal triple [message_end, turn_end, agent_end] for every status
137
+ * ("complete" | "interrupted" | "error"): the wire run bracket that
138
+ * agent_start/turn_start opened at message.start closes here.
139
+ */
140
+ function messageComplete(p) {
141
+ const status = asText(p.status);
142
+ const usage = asRecord(p.usage);
143
+ const content = [];
144
+ const message = { role: "assistant", content };
145
+ if (status === "error") {
146
+ const partial = asText(p.partial);
147
+ if (partial !== "")
148
+ content.push({ type: "text", text: partial });
149
+ message.stopReason = "error";
150
+ const error = asText(p.error);
151
+ if (error !== "")
152
+ message.errorMessage = error;
153
+ }
154
+ else {
155
+ content.push({ type: "text", text: asText(p.text) });
156
+ const reasoning = asText(p.reasoning);
157
+ if (reasoning !== "")
158
+ content.push({ type: "thinking", thinking: reasoning });
159
+ }
160
+ const end = { type: "message_end", message };
161
+ if (usage !== null)
162
+ end.usage = usage;
163
+ const data = {};
164
+ if (usage !== null)
165
+ data.usage = usage;
166
+ if (status !== "")
167
+ data.status = status;
168
+ const turnEnd = Object.keys(data).length === 0 ? { type: "turn_end" } : { type: "turn_end", data };
169
+ return [end, turnEnd, { type: "agent_end" }];
170
+ }
171
+ /**
172
+ * Project one Hermes gateway event frame (duck-typed: the client passes
173
+ * `params.type` and `params.payload` — payload is OPTIONAL, `message.start`
174
+ * carries none) onto the omp wire vocabulary. Returns one event, an ordered
175
+ * array, or null for ignorable/malformed input. Never throws.
176
+ */
177
+ export function projectGatewayEvent(type, payload, ctx) {
178
+ try {
179
+ if (typeof type !== "string")
180
+ return null;
181
+ const turnOpen = ctx !== null && typeof ctx === "object" && ctx.turnOpen === true;
182
+ const openAssistant = ctx !== null && typeof ctx === "object" && ctx.openAssistant === true;
183
+ switch (type) {
184
+ case "message.start": {
185
+ // Turn-open bracket: no payload key exists on this frame.
186
+ const start = assistantMessageStart();
187
+ return turnOpen ? start : [{ type: "agent_start" }, { type: "turn_start" }, start];
188
+ }
189
+ case "message.delta": {
190
+ const p = asRecord(payload);
191
+ if (p === null)
192
+ return null;
193
+ const update = textDelta(asText(p.text));
194
+ return openAssistant ? update : [assistantMessageStart(), update];
195
+ }
196
+ case "thinking.delta":
197
+ case "reasoning.delta":
198
+ // Folded into the final message_end via message.complete.reasoning;
199
+ // live display is the client's accumulation. Never wire.
200
+ return null;
201
+ case "tool.start": {
202
+ const p = asRecord(payload);
203
+ if (p === null)
204
+ return null;
205
+ const args = asRecord(p.args) ?? {};
206
+ // args_text (verbose sessions) is the RAW string — keep it verbatim,
207
+ // mirroring codex's raw mcp argumentsJson seam.
208
+ const argumentsJson = typeof p.args_text === "string" ? p.args_text : JSON.stringify(args);
209
+ return {
210
+ type: "tool_execution_start",
211
+ toolCallId: asText(p.tool_id),
212
+ toolName: asText(p.name),
213
+ args,
214
+ argumentsJson,
215
+ };
216
+ }
217
+ case "tool.complete": {
218
+ const p = asRecord(payload);
219
+ if (p === null)
220
+ return null;
221
+ const end = {
222
+ type: "tool_execution_end",
223
+ toolCallId: asText(p.tool_id),
224
+ toolName: asText(p.name),
225
+ result: { content: [{ type: "text", text: renderToolResult(p) }], isError: false },
226
+ isError: false,
227
+ };
228
+ if (typeof p.duration_s === "number")
229
+ end.duration_s = p.duration_s;
230
+ return end;
231
+ }
232
+ case "message.complete": {
233
+ const p = asRecord(payload);
234
+ if (p === null)
235
+ return null;
236
+ return messageComplete(p);
237
+ }
238
+ case "error": {
239
+ const p = asRecord(payload);
240
+ if (p === null || !turnOpen)
241
+ return null; // abnormal finish, only mid-turn
242
+ const message = asText(p.message);
243
+ return message === "" ? { type: "agent_end" } : { type: "agent_end", data: { error: { message } } };
244
+ }
245
+ case "session.title": {
246
+ // Gateway-owned title → session_title wire event; the AGENT mirrors it
247
+ // as `session/title` (pi pattern). Empty/missing titles stay null.
248
+ const p = asRecord(payload);
249
+ const title = typeof p?.title === "string" ? p.title : "";
250
+ return title === "" ? null : { type: "session_title", name: title };
251
+ }
252
+ case "todo.updated": {
253
+ // Whole-list todo snapshot {todos, revision} → todo_updated with
254
+ // DSH-shaped items; the AGENT appends the log-only `todo/write`.
255
+ // Mapping: trimmed non-empty content; status ∈ {pending, in_progress,
256
+ // completed} (gateway `cancelled`/unknown statuses drop); duplicate
257
+ // content drops (the DSH todo/write invariant forbids repeats). A
258
+ // well-formed empty list passes through — a real clear (revision ≥ 1).
259
+ const p = asRecord(payload);
260
+ if (p === null || !Array.isArray(p.todos))
261
+ return null;
262
+ const todos = [];
263
+ const seenContent = new Set();
264
+ for (const item of p.todos) {
265
+ if (item === null || typeof item !== "object" || Array.isArray(item))
266
+ continue;
267
+ const raw = item;
268
+ const content = typeof raw.content === "string" ? raw.content.trim() : "";
269
+ if (content === "" || seenContent.has(content))
270
+ continue;
271
+ const status = raw.status;
272
+ if (status !== "pending" && status !== "in_progress" && status !== "completed")
273
+ continue;
274
+ seenContent.add(content);
275
+ todos.push({ content, status });
276
+ }
277
+ return { type: "todo_updated", todos };
278
+ }
279
+ default:
280
+ // session.info / session.usage / status.update /
281
+ // approval.request / sessions.changed /
282
+ // gateway.ready / message.interim / unknown → ignorable (S5).
283
+ return null;
284
+ }
285
+ }
286
+ catch {
287
+ return null;
288
+ }
289
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * DSH-session ↔ Hermes-gateway-session identity map (the "mapping only"
3
+ * model, 2026-09-17).
4
+ *
5
+ * The DSH session id is the authority: it is supplied by the harness at
6
+ * create/resume and is what the API layer routes on. The gateway's session id
7
+ * only materializes after the first prompt, so the pairing is persisted here,
8
+ * in an adapter-owned JSON file in the hermes world's DSH-side app dir (NOT
9
+ * hermes data — the hermes runtime keeps its native `~/.hermes` home since the
10
+ * 2026-09-17 user ruling; DSH bookkeeping never lives in the runtime's native
11
+ * tree):
12
+ *
13
+ * `<appDir>/dsh-sessions.json` (`<DSH home>/agents/hermes/dsh-sessions.json`)
14
+ *
15
+ * Shape: `{ "<dshSessionId>": { gatewaySessionId, cwd, createdAt, preset } }` —
16
+ * `gatewaySessionId` is `null` until the gateway session materializes (the
17
+ * first prompt is pending). Writes are atomic (temp file + rename); reads are
18
+ * fail-soft (malformed JSON reads as empty) and never throw, so a corrupt map
19
+ * degrades to "unknown session" rather than breaking every operation.
20
+ */
21
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { basename, dirname, join, resolve } from "node:path";
24
+ /** Prod homes that must never be resolved into (repo red line). */
25
+ const PROD_HOMES = [join(homedir(), ".dsh"), join(homedir(), ".superd")];
26
+ function assertNotProdHome(path, label) {
27
+ if (PROD_HOMES.includes(path) || PROD_HOMES.some((p) => path.startsWith(`${p}/`))) {
28
+ throw new Error(`hermes-store: refusing prod home as ${label}: ${path}`);
29
+ }
30
+ }
31
+ /**
32
+ * The adapter's DSH-side state dir — `<dshHome>/agents/hermes`. This is DSH-SIDE
33
+ * adapter state (the identity map), NOT hermes data: hermes keeps its native
34
+ * `~/.hermes` home (user ruling 2026-09-17) and this module never touches it.
35
+ *
36
+ * Two forms (2026-09-17 r2 fix):
37
+ * - Form-A: the boot-provided home (or `$DSH_HOME`) is the DSH home ROOT —
38
+ * the per-agent world dir is NESTED, so the map lands at
39
+ * `<root>/agents/hermes/dsh-sessions.json`.
40
+ * - Form-B: the boot-provided home is ALREADY the world home
41
+ * (`<root>/agents/hermes`) — passed through as-is, never double-nested.
42
+ * Precedence: explicit home (boot-provided `dshHomePath`), then `$DSH_HOME`,
43
+ * then the line's historical test-home default. Prod-guarded.
44
+ */
45
+ export function resolveHermesAppDir(home) {
46
+ const base = resolve(home ?? process.env.DSH_HOME ?? join(process.cwd(), ".tests", "aw"));
47
+ // Form-B world home is already `<root>/agents/hermes` — never double-nest.
48
+ const dir = basename(base) === "hermes" && basename(dirname(base)) === "agents" ? base : join(base, "agents", "hermes");
49
+ assertNotProdHome(dir, "hermes app dir");
50
+ return dir;
51
+ }
52
+ /** The map file's path inside the adapter's DSH-side app dir. */
53
+ export function sessionMapPath(home) {
54
+ return join(resolveHermesAppDir(home), "dsh-sessions.json");
55
+ }
56
+ /** Read the map. Fail-soft: a missing or malformed file reads as `{}`. */
57
+ export function readSessionMap(home) {
58
+ let raw;
59
+ try {
60
+ raw = readFileSync(sessionMapPath(home), "utf8");
61
+ }
62
+ catch {
63
+ return {};
64
+ }
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(raw);
68
+ }
69
+ catch {
70
+ return {};
71
+ }
72
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
73
+ return {};
74
+ const out = {};
75
+ for (const [id, value] of Object.entries(parsed)) {
76
+ const record = coerceRecord(value);
77
+ if (record !== undefined)
78
+ out[id] = record;
79
+ }
80
+ return out;
81
+ }
82
+ /** Validate one entry; malformed entries are dropped (fail-soft). */
83
+ function coerceRecord(value) {
84
+ if (value === null || typeof value !== "object" || Array.isArray(value))
85
+ return undefined;
86
+ const raw = value;
87
+ const cwd = raw["cwd"];
88
+ const createdAt = raw["createdAt"];
89
+ if (typeof cwd !== "string" || cwd === "")
90
+ return undefined;
91
+ const gatewaySessionId = raw["gatewaySessionId"];
92
+ const gatewayStoredSessionId = raw["gatewayStoredSessionId"];
93
+ const preset = raw["preset"];
94
+ return {
95
+ gatewaySessionId: typeof gatewaySessionId === "string" && gatewaySessionId !== "" ? gatewaySessionId : null,
96
+ gatewayStoredSessionId: typeof gatewayStoredSessionId === "string" && gatewayStoredSessionId !== "" ? gatewayStoredSessionId : null,
97
+ cwd,
98
+ createdAt: typeof createdAt === "number" && Number.isFinite(createdAt) ? createdAt : 0,
99
+ preset: typeof preset === "string" && preset !== "" ? preset : null,
100
+ };
101
+ }
102
+ /** One session's record, or `undefined` when the id is unknown. */
103
+ export function sessionRecord(home, dshSessionId) {
104
+ return readSessionMap(home)[dshSessionId];
105
+ }
106
+ /**
107
+ * Insert or merge one session's record and persist the map atomically
108
+ * (temp file + rename, same directory). Merge semantics: an omitted `patch`
109
+ * field keeps the stored value; `gatewaySessionId: null` explicitly records
110
+ * "not yet materialized" (first prompt pending). Throws only when the write
111
+ * itself fails — the caller owns that decision (create treats an unwritable
112
+ * map as fatal; the observation path logs and continues).
113
+ */
114
+ export function upsertSession(home, dshSessionId, patch) {
115
+ const path = sessionMapPath(home);
116
+ const map = readSessionMap(home);
117
+ const previous = map[dshSessionId];
118
+ const next = {
119
+ gatewaySessionId: patch.gatewaySessionId !== undefined ? patch.gatewaySessionId : previous?.gatewaySessionId ?? null,
120
+ gatewayStoredSessionId: patch.gatewayStoredSessionId !== undefined ? patch.gatewayStoredSessionId : previous?.gatewayStoredSessionId ?? null,
121
+ cwd: patch.cwd !== undefined ? patch.cwd : previous?.cwd ?? "",
122
+ createdAt: patch.createdAt !== undefined ? patch.createdAt : previous?.createdAt ?? Date.now(),
123
+ preset: patch.preset !== undefined ? patch.preset : previous?.preset ?? null,
124
+ };
125
+ map[dshSessionId] = next;
126
+ writeMapAtomic(path, map);
127
+ return next;
128
+ }
129
+ /** Drop one session's record (idempotent; failure to persist rethrows). */
130
+ export function forgetSession(home, dshSessionId) {
131
+ const path = sessionMapPath(home);
132
+ if (!existsSync(path))
133
+ return;
134
+ const map = readSessionMap(home);
135
+ if (map[dshSessionId] === undefined)
136
+ return;
137
+ delete map[dshSessionId];
138
+ writeMapAtomic(path, map);
139
+ }
140
+ /** Atomic persist: temp file (mode 0600) + same-directory rename. */
141
+ function writeMapAtomic(path, map) {
142
+ const dir = dirname(path);
143
+ mkdirSync(dir, { recursive: true });
144
+ const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
145
+ writeFileSync(temp, `${JSON.stringify(map, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
146
+ try {
147
+ renameSync(temp, path);
148
+ }
149
+ catch (error) {
150
+ try {
151
+ rmSync(temp, { force: true });
152
+ }
153
+ catch {
154
+ // The rename failure is primary.
155
+ }
156
+ throw error;
157
+ }
158
+ }
@@ -0,0 +1,53 @@
1
+ export class Inbox {
2
+ #nextTurn = [];
3
+ #nextStep = [];
4
+ get nextTurn() {
5
+ return this.#nextTurn;
6
+ }
7
+ get nextStep() {
8
+ return this.#nextStep;
9
+ }
10
+ clear() {
11
+ this.#nextTurn = [];
12
+ this.#nextStep = [];
13
+ }
14
+ append(target, message) {
15
+ this.#list(target).push(message);
16
+ }
17
+ prepend(target, message) {
18
+ this.#list(target).unshift(message);
19
+ }
20
+ replace(messageId, newMessage) {
21
+ const list = this.#find(messageId);
22
+ if (list === undefined)
23
+ return false;
24
+ const index = list.findIndex((message) => message.id === messageId);
25
+ if (index < 0)
26
+ return false;
27
+ list.splice(index, 1, newMessage);
28
+ return true;
29
+ }
30
+ remove(messageId) {
31
+ const list = this.#find(messageId);
32
+ if (list === undefined)
33
+ return false;
34
+ const index = list.findIndex((message) => message.id === messageId);
35
+ if (index < 0)
36
+ return false;
37
+ list.splice(index, 1);
38
+ return true;
39
+ }
40
+ splice(target, start, deleteCount, inserted) {
41
+ return this.#list(target).splice(start, deleteCount, ...inserted);
42
+ }
43
+ #list(target) {
44
+ return target === "next-turn" ? this.#nextTurn : this.#nextStep;
45
+ }
46
+ #find(messageId) {
47
+ if (this.#nextTurn.some((message) => message.id === messageId))
48
+ return this.#nextTurn;
49
+ if (this.#nextStep.some((message) => message.id === messageId))
50
+ return this.#nextStep;
51
+ return undefined;
52
+ }
53
+ }