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.
- package/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- 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
|
+
}
|