@adhdev/daemon-core 0.9.82-rc.160 → 0.9.82-rc.161

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 (63) hide show
  1. package/dist/cli-adapter-types.d.ts +14 -1
  2. package/dist/commands/mesh-coordinator.d.ts +72 -1
  3. package/dist/config/chat-history.d.ts +2 -0
  4. package/dist/config/mesh-config.d.ts +3 -0
  5. package/dist/index.d.ts +11 -0
  6. package/dist/index.js +4923 -1411
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +4975 -1476
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  11. package/dist/mesh/coordinator-registry.d.ts +35 -1
  12. package/dist/providers/cli-provider-instance.d.ts +1 -1
  13. package/dist/providers/contracts.d.ts +48 -0
  14. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +1 -1
  15. package/dist/providers/native-history/claude-cli-transcript.d.ts +1 -1
  16. package/dist/providers/native-history/codex-cli-transcript.d.ts +1 -1
  17. package/dist/providers/native-history/dispatcher.d.ts +24 -0
  18. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  19. package/dist/providers/native-history/index.d.ts +2 -0
  20. package/dist/providers/spec/adapter.d.ts +56 -0
  21. package/dist/providers/spec/cli-adapter.d.ts +76 -0
  22. package/dist/providers/spec/driver.d.ts +148 -0
  23. package/dist/providers/spec/evaluator.d.ts +47 -0
  24. package/dist/providers/spec/loader.d.ts +14 -0
  25. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  26. package/dist/providers/spec/route.d.ts +4 -0
  27. package/dist/providers/spec/schema.gen.d.ts +507 -0
  28. package/dist/providers/spec/types.d.ts +211 -0
  29. package/dist/repo-mesh-types.d.ts +33 -1
  30. package/dist/sessions/registry.d.ts +3 -0
  31. package/package.json +2 -1
  32. package/src/cli-adapter-types.ts +15 -1
  33. package/src/commands/chat-commands.ts +150 -12
  34. package/src/commands/cli-manager.ts +11 -0
  35. package/src/commands/mesh-coordinator.ts +235 -1
  36. package/src/commands/router.ts +238 -50
  37. package/src/config/chat-history.ts +11 -3
  38. package/src/config/mesh-config.ts +16 -1
  39. package/src/index.ts +19 -0
  40. package/src/mesh/coordinator-prompt.ts +164 -8
  41. package/src/mesh/coordinator-registry.ts +50 -4
  42. package/src/providers/cli-provider-instance.ts +8 -3
  43. package/src/providers/contracts.ts +53 -0
  44. package/src/providers/native-history/antigravity-cli-transcript.ts +2 -2
  45. package/src/providers/native-history/claude-cli-transcript.ts +1 -1
  46. package/src/providers/native-history/codex-cli-transcript.ts +1 -1
  47. package/src/providers/native-history/dispatcher.ts +227 -0
  48. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  49. package/src/providers/native-history/index.ts +7 -0
  50. package/src/providers/provider-loader.ts +126 -3
  51. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +13 -0
  52. package/src/providers/spec/adapter.ts +153 -0
  53. package/src/providers/spec/cli-adapter.ts +318 -0
  54. package/src/providers/spec/driver.ts +498 -0
  55. package/src/providers/spec/evaluator.ts +268 -0
  56. package/src/providers/spec/loader.ts +130 -0
  57. package/src/providers/spec/native-history-executor.ts +612 -0
  58. package/src/providers/spec/route.ts +51 -0
  59. package/src/providers/spec/schema.gen.ts +507 -0
  60. package/src/providers/spec/schema.json +210 -0
  61. package/src/providers/spec/types.ts +230 -0
  62. package/src/repo-mesh-types.ts +33 -1
  63. package/src/sessions/registry.ts +3 -0
@@ -0,0 +1,211 @@
1
+ export type Size = number | string;
2
+ export interface Section {
3
+ id: string;
4
+ from_top?: Size;
5
+ from_bottom?: Size;
6
+ until?: {
7
+ section: string;
8
+ };
9
+ }
10
+ export interface SectionRegex {
11
+ section?: string;
12
+ regex: string;
13
+ flags?: string;
14
+ }
15
+ export interface SectionPattern {
16
+ section?: string;
17
+ pattern: string;
18
+ flags?: string;
19
+ }
20
+ export interface ModalButtonsRule {
21
+ section?: string;
22
+ pattern: string;
23
+ flags?: string;
24
+ key_for_index: string;
25
+ min_count?: number;
26
+ }
27
+ export interface SpecState {
28
+ id: string;
29
+ label: string;
30
+ when: SectionRegex;
31
+ extract_title?: SectionRegex;
32
+ modal_buttons?: ModalButtonsRule;
33
+ }
34
+ export type ControlAction = {
35
+ type: 'send_keys';
36
+ keys: string;
37
+ } | {
38
+ type: 'open_picker';
39
+ trigger_keys: string;
40
+ wait_for: SectionRegex;
41
+ extract_choices: SectionPattern;
42
+ submit_key: string;
43
+ } | {
44
+ type: 'attach_image';
45
+ method: 'tempfile_then_keys';
46
+ keys_template: string;
47
+ };
48
+ export interface Control {
49
+ id: string;
50
+ label: string;
51
+ visible_when_state?: string[];
52
+ action: ControlAction;
53
+ }
54
+ export interface NotificationRule {
55
+ id: string;
56
+ when_state: string;
57
+ title: string;
58
+ body?: string;
59
+ }
60
+ export interface DelegateTrigger {
61
+ id: string;
62
+ when_state: string;
63
+ after_duration_ms?: number;
64
+ task_template: string;
65
+ }
66
+ /**
67
+ * Native history config — three modes, picked by which fields are present:
68
+ *
69
+ * 1. `reader`: built-in reader id (claude-cli / codex-cli / antigravity-cli / hermes-cli).
70
+ * Backwards-compatible path used by the four shipped providers.
71
+ *
72
+ * 2. `source`: declarative source descriptor. The daemon's spec native-history
73
+ * executor reads the on-disk store (jsonl file / sqlite db) directly using
74
+ * paths + jsonpath maps from this block. New providers should prefer this
75
+ * mode — no daemon changes required.
76
+ *
77
+ * 3. `override_path`: relative path to a provider-supplied reader module
78
+ * (e.g. "./native_history/index.js"). Escape hatch for formats too
79
+ * exotic for the declarative executor. The module must default-export
80
+ * a function with the signature
81
+ * (input: NativeHistoryInput) => NativeHistoryResult | null
82
+ * and runs under provider-script-root sandboxing (same gate as
83
+ * scripts.js).
84
+ *
85
+ * The three modes are mutually exclusive — set exactly one. Loader rejects
86
+ * specs that set zero or more than one.
87
+ */
88
+ export interface NativeHistoryConfig {
89
+ reader?: 'claude-cli' | 'codex-cli' | 'antigravity-cli' | 'hermes-cli';
90
+ source?: NativeHistorySource;
91
+ override_path?: string;
92
+ }
93
+ export type NativeHistorySource = NativeHistoryJsonlSource | NativeHistorySqliteSource;
94
+ /**
95
+ * JSONL source — each line in the resolved file is one record.
96
+ *
97
+ * `path` accepts ~/, environment variables (${HOME}), and a small set of
98
+ * runtime variables:
99
+ * {cwd} — the session's working directory, raw
100
+ * {cwd_dashed} — cwd with `/` replaced by `-` (claude's per-project key)
101
+ * {session_id} — providerSessionId, when known
102
+ * {yyyy} {mm} {dd} — UTC date components
103
+ *
104
+ * When `path` resolves to a directory, the daemon picks the newest file
105
+ * matching `file_pattern` whose mtime is inside `recent_window_ms` (default
106
+ * 5min) — prevents the wrong session's transcript from leaking through.
107
+ */
108
+ export interface NativeHistoryJsonlSource {
109
+ kind: 'jsonl';
110
+ path: string;
111
+ file_pattern?: string;
112
+ recent_window_ms?: number;
113
+ session_id_from?: 'filename_uuid' | 'first_record';
114
+ session_id_path?: string;
115
+ message_filter?: {
116
+ where: string;
117
+ };
118
+ message_map: NativeHistoryMessageMap;
119
+ }
120
+ /**
121
+ * SQLite source — provider runs two queries on a read-only handle: one to
122
+ * pick the session row (`session_query` — first row's first column is the
123
+ * sessionId), then one to fetch messages (`message_query` — `?` is bound
124
+ * to the sessionId). Output rows are projected through `message_map`.
125
+ */
126
+ export interface NativeHistorySqliteSource {
127
+ kind: 'sqlite';
128
+ path: string;
129
+ session_query: string;
130
+ message_query: string;
131
+ message_map: NativeHistoryMessageMap;
132
+ }
133
+ /**
134
+ * Maps a source record (jsonl line object / sqlite row) to a daemon
135
+ * native-history message. Values are jsonpath-lite strings: `$.foo.bar`
136
+ * or `$.items[0].text`. Literal strings (no leading `$`) pass through.
137
+ *
138
+ * `content_strip` lets a spec drop wrapper tags that the agent injects
139
+ * into its own transcript (claude's `<local-command-caveat>`, agy's
140
+ * `<USER_REQUEST>` / `<ADDITIONAL_METADATA>` / `<USER_SETTINGS_CHANGE>`).
141
+ * Each entry is a tag name; the executor removes `<tag>…</tag>` segments
142
+ * non-greedily before the value is handed to the dashboard. After all
143
+ * strips, leading/trailing whitespace is trimmed. If the result is empty
144
+ * the message is dropped.
145
+ */
146
+ export interface NativeHistoryMessageMap {
147
+ role: string;
148
+ content: string;
149
+ /**
150
+ * Tags whose entire `<tag>…</tag>` segment is removed from content.
151
+ * Use for noise wrappers the agent ships alongside the real message
152
+ * (claude's `<local-command-caveat>`, agy's `<ADDITIONAL_METADATA>`).
153
+ */
154
+ content_strip?: string[];
155
+ /**
156
+ * Tags whose open/close markers are removed but inner content is kept.
157
+ * Use when the *real* user/assistant message is wrapped in a tag
158
+ * the agent uses for routing (agy wraps user input in `<USER_REQUEST>`).
159
+ */
160
+ content_unwrap?: string[];
161
+ timestamp_ms?: string;
162
+ kind?: string;
163
+ }
164
+ export interface CliSpec {
165
+ $schema: 'adhdev:cli/spec@1';
166
+ id: string;
167
+ name: string;
168
+ binary: string;
169
+ spawn_args?: string[];
170
+ env?: Record<string, string>;
171
+ /**
172
+ * Optional version constraint this spec is authored for, e.g. ">=2.1.0".
173
+ * Pure metadata used by tooling/UI — the actual version-to-spec match
174
+ * happens in provider-loader via the manifest's compatibility array.
175
+ * Spec authors include this so a misrouted spec is easy to spot.
176
+ */
177
+ cli_version_range?: string;
178
+ send_message: {
179
+ submit_key: string;
180
+ delay_ms_before_submit?: number;
181
+ delay_ms_per_char?: number;
182
+ };
183
+ layout: {
184
+ sections: Section[];
185
+ };
186
+ states: SpecState[];
187
+ default_state: string;
188
+ control_bar?: Control[];
189
+ notifications?: NotificationRule[];
190
+ delegate?: DelegateTrigger[];
191
+ native_history?: NativeHistoryConfig;
192
+ /**
193
+ * Per-spec debounce knobs. Defaults are conservative (busy_hold_ms
194
+ * 6000) and live in SpecDriver. Spec authors override here when a
195
+ * particular CLI flickers slower/faster than the default.
196
+ */
197
+ debounce?: {
198
+ /** Min time to stay in busy after the evaluator last reported it.
199
+ * Absorbs per-frame flicker in TUIs that stream output through
200
+ * the same region as the spinner. */
201
+ busy_hold_ms?: number;
202
+ /** Min time after start() before a send_message is allowed to
203
+ * reach the PTY. Banner paints + auth flows + skill listings
204
+ * can keep the agent unable to accept input for several seconds
205
+ * even though the idle regex matches a transient empty prompt.
206
+ * Send_messages issued before this window are queued and drained
207
+ * once the window passes and an idle state has actually been
208
+ * observed. */
209
+ startup_grace_ms?: number;
210
+ };
211
+ }
@@ -202,7 +202,28 @@ export interface RepoMeshCoordinatorConfig {
202
202
  providerType?: string;
203
203
  /** Preferred node to run coordinator on (null = auto) */
204
204
  preferredNodeId?: string;
205
- /** Additional system prompt context for coordinator */
205
+ /**
206
+ * Full mesh-level override for the coordinator system prompt. When set,
207
+ * replaces the daemon's rendered default and any user-file override
208
+ * (~/.adhdev/coordinator-prompts/<cli>.md). The per-launch
209
+ * extraSystemPrompt still composes on top — it always lands last as
210
+ * Additional Context. Supports the same {{placeholders}} the daemon's
211
+ * default template uses ({{meshName}}, {{repo}}, {{nodes}}, …).
212
+ */
213
+ systemPromptOverride?: string;
214
+ /**
215
+ * Mesh-level append. Composes after whichever base prompt won
216
+ * (override → user-file override → daemon default). Use this when you
217
+ * want extra rules for THIS mesh but otherwise the standard prompt is
218
+ * fine. Stacks with the user-file append (`<cli>.append.md`) — both
219
+ * apply if both are set.
220
+ */
221
+ systemPromptAppend?: string;
222
+ /**
223
+ * @deprecated Use systemPromptAppend. Kept as a fallback alias so
224
+ * existing meshes.json files keep working without a migration step;
225
+ * the daemon prefers systemPromptAppend when both are present.
226
+ */
206
227
  systemPromptSuffix?: string;
207
228
  }
208
229
  /**
@@ -234,6 +255,15 @@ export interface LocalMeshNodeEntry {
234
255
  machineId?: string;
235
256
  userOverrides: Partial<RepoMeshNodeCapabilities>;
236
257
  policy: RepoMeshNodePolicy;
258
+ /**
259
+ * Per-node instruction surfaced in the coordinator prompt so the LLM
260
+ * knows what each node is for (e.g. "this is the staging mirror — run
261
+ * only smoke tests here", or "use opus on this node, sonnet elsewhere").
262
+ * Empty/missing: omitted silently from the rendered prompt, no rule
263
+ * line about it gets added. The coordinator forwards/honors it when
264
+ * delegating; we don't enforce it at the daemon level.
265
+ */
266
+ systemPrompt?: string;
237
267
  /** For single-machine mesh: same daemon, different worktree */
238
268
  isLocalWorktree?: boolean;
239
269
  /** Branch this worktree tracks (set when created via clone_mesh_node) */
@@ -305,6 +335,8 @@ export interface RepoMeshNodeStatus {
305
335
  machineStatus?: string;
306
336
  isLocalWorktree?: boolean;
307
337
  worktreeBranch?: string;
338
+ /** Mirrored from LocalMeshNodeEntry.systemPrompt for coordinator-prompt rendering. */
339
+ systemPrompt?: string;
308
340
  health: RepoMeshNodeHealth;
309
341
  git?: GitRepoStatus;
310
342
  /**
@@ -7,6 +7,9 @@ export interface SessionRuntimeTarget {
7
7
  cdpManagerKey?: string;
8
8
  adapterKey?: string;
9
9
  instanceKey?: string;
10
+ /** Wall clock at register time. native-history readers use it as a
11
+ * cutoff so a fresh session can't show records from a prior one. */
12
+ spawnedAtMs?: number;
10
13
  }
11
14
  export declare class SessionRegistry {
12
15
  private readonly bySessionId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.160",
3
+ "version": "0.9.82-rc.161",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -48,6 +48,7 @@
48
48
  "dependencies": {
49
49
  "@adhdev/session-host-core": "*",
50
50
  "@agentclientprotocol/sdk": "^0.16.1",
51
+ "@xterm/headless": "^6.0.0",
51
52
  "@xterm/xterm": "^6.0.0",
52
53
  "ajv": "^8.20.0",
53
54
  "ajv-formats": "^3.0.1",
@@ -13,6 +13,9 @@ export interface CliAdapterStatus {
13
13
  message: string;
14
14
  buttons: string[];
15
15
  } | null;
16
+ providerSessionId?: string;
17
+ errorMessage?: string;
18
+ errorReason?: string;
16
19
  }
17
20
 
18
21
  export interface AcpAdapterHandle {
@@ -40,7 +43,7 @@ export interface CliAdapter {
40
43
  spawn(): Promise<void>;
41
44
  sendMessage(text: string, options?: { force?: boolean }): Promise<void>;
42
45
  forceSendMessage?(text: string): Promise<void>;
43
- getStatus(): CliAdapterStatus;
46
+ getStatus(options?: { allowParse?: boolean }): CliAdapterStatus;
44
47
  getScriptParsedStatus?(): unknown;
45
48
  getDebugSnapshot?(): unknown;
46
49
  invokeScript?(scriptName: string, args?: Record<string, unknown>): Promise<unknown>;
@@ -63,4 +66,15 @@ export interface CliAdapter {
63
66
  setOnPtyData?(callback: (data: string) => void): void;
64
67
  writeRaw?(data: string): void;
65
68
  resize?(cols: number, rows: number): void;
69
+ // ── Runtime metadata used by CliProviderInstance for session tracking ──
70
+ getRuntimeMetadata?(): unknown;
71
+ updateRuntimeMeta?(meta: Record<string, unknown>): void;
72
+ refreshProviderDefinition?(provider: unknown): void;
73
+ // ── Optional auxiliary fields some daemon paths read off the status ──
74
+ }
75
+
76
+ export interface CliAdapterStatusOptional {
77
+ providerSessionId?: string;
78
+ errorMessage?: string;
79
+ errorReason?: string;
66
80
  }
@@ -15,6 +15,7 @@ import { validateReadChatResultPayload } from '../providers/read-chat-contract.j
15
15
  import { pickApprovalButton } from '../providers/approval-utils.js';
16
16
  import type { ProviderInstance } from '../providers/provider-instance.js';
17
17
  import { isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
18
+ import { getCoordinatorForSession } from '../mesh/coordinator-registry.js';
18
19
  import { LOG, getRecentLogs } from '../logging/logger.js';
19
20
  import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
20
21
  import { buildChatMessageSignature, hashSignatureParts } from '../chat/chat-signatures.js';
@@ -324,6 +325,82 @@ function shouldPreserveNativeIdentity(providerType: string, sessionId: string, m
324
325
  return true;
325
326
  }
326
327
 
328
+ /**
329
+ * Drop the synthetic "user" message some CLIs surface in their native
330
+ * transcript when the daemon injects a coordinator system prompt
331
+ * (codex puts the AGENTS.md / developer_instructions block in as
332
+ * role=user; agy/claude/hermes have similar artifacts). The user can
333
+ * opt back into seeing it via the provider setting
334
+ * `showCoordinatorSystemPrompt`. Default is off — the prompt is still
335
+ * fully visible from the chat-header ⓘ "Session info" dialog.
336
+ *
337
+ * Matching rules:
338
+ * 1. Setting must be off (default).
339
+ * 2. There must be a registered coordinator entry for the session.
340
+ * 3. The candidate message is filtered when its role is user OR
341
+ * system and its content either contains the prompt body verbatim,
342
+ * OR contains the well-known coordinator marker
343
+ * `adhdev-mesh-coordinator-prompt`. The marker covers context-file
344
+ * cases (agy AGENTS.md / gemini GEMINI.md) where the CLI may wrap
345
+ * its own preamble around our block. Verbatim-content covers
346
+ * codex's developer_instructions echo.
347
+ *
348
+ * Returns the messages array unchanged when none of the rules match,
349
+ * so this is safe to apply unconditionally to every read_chat result.
350
+ */
351
+ function maybeHideCoordinatorPromptMessage(
352
+ h: CommandHelpers,
353
+ providerType: string,
354
+ sessionId: string | undefined,
355
+ messages: ChatMessage[],
356
+ ): ChatMessage[] {
357
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
358
+ if (!sessionId) return messages;
359
+ const loader = h.ctx?.providerLoader;
360
+ if (!loader) return messages;
361
+ let showSetting: unknown = undefined;
362
+ try {
363
+ showSetting = (loader as any).getSettingValue?.(providerType, 'showCoordinatorSystemPrompt');
364
+ } catch { /* unknown setting key for this provider — fall through */ }
365
+ if (showSetting === true) return messages;
366
+ const coord = getCoordinatorForSession(sessionId);
367
+ if (!coord) return messages;
368
+ const promptBody = typeof coord.systemPrompt === 'string' ? coord.systemPrompt : '';
369
+ const MARKER = 'adhdev-mesh-coordinator-prompt';
370
+ const filtered = messages.filter(m => {
371
+ const role = String((m as any)?.role || '').toLowerCase();
372
+ if (role !== 'user' && role !== 'system') return true;
373
+ const content = flattenContent((m as any)?.content);
374
+ if (!content) return true;
375
+ if (content.includes(MARKER)) return false;
376
+ if (promptBody && content.includes(promptBody.slice(0, Math.min(400, promptBody.length)))) return false;
377
+ return true;
378
+ });
379
+ if (filtered.length !== messages.length) {
380
+ LOG.debug('ChatFilter', `[${providerType}] hid ${messages.length - filtered.length} coordinator-prompt message(s) from ${sessionId}`);
381
+ }
382
+ return filtered;
383
+ }
384
+
385
+ /**
386
+ * Convenience wrapper used at every native-history call site: normalize +
387
+ * conditionally drop the coordinator system-prompt message. Avoids
388
+ * duplicating the filter at four read_chat code paths.
389
+ */
390
+ function normalizeAndFilterNativeHistory(
391
+ h: CommandHelpers,
392
+ providerType: string,
393
+ args: any,
394
+ messages: ChatMessage[],
395
+ nativeSessionId?: string,
396
+ ): ChatMessage[] {
397
+ const normalized = normalizeNativeHistoryMessages(providerType, messages, nativeSessionId);
398
+ const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId
399
+ : typeof args?.sessionId === 'string' ? args.sessionId
400
+ : undefined;
401
+ return maybeHideCoordinatorPromptMessage(h, providerType, sessionId, normalized);
402
+ }
403
+
327
404
  function normalizeNativeHistoryMessages(providerType: string, messages: ChatMessage[], nativeSessionId?: string): ChatMessage[] {
328
405
  let turnIndex = 0;
329
406
  return normalizeChatMessages(messages).map((message, index) => {
@@ -1003,6 +1080,41 @@ function hasSafeNativeHistoryMapping(args: {
1003
1080
  // other. historySessionId (the provider-native session key) is required to
1004
1081
  // establish ownership. hasSafeNativeHistoryMapping() enforces the same
1005
1082
  // invariant after the read; both guards must hold for native history to be used.
1083
+ /**
1084
+ * Pull the session's spawnedAtMs out of the registry. Native-history
1085
+ * file pickers use it as a "files older than this can't be from this
1086
+ * session" floor; without it a fresh dashboard view would inherit the
1087
+ * previous session's transcript whenever its file happened to be the
1088
+ * newest match. Returns undefined when the session isn't registered
1089
+ * (e.g. read_chat before the live session was wired up) — the executor
1090
+ * treats undefined as "no floor".
1091
+ */
1092
+ function sessionStartedAtMsFromRegistry(h: CommandHelpers, targetSessionId: string | undefined): number | undefined {
1093
+ const sid = typeof targetSessionId === 'string' ? targetSessionId.trim() : '';
1094
+ if (!sid) return undefined;
1095
+ const target = h.ctx?.sessionRegistry?.get?.(sid);
1096
+ return typeof target?.spawnedAtMs === 'number' ? target.spawnedAtMs : undefined;
1097
+ }
1098
+
1099
+ /**
1100
+ * Pull the env vars the daemon set when it spawned this session's CLI.
1101
+ * Mesh coordinator points hermes at a per-coordinator HERMES_HOME so
1102
+ * the native-history reader needs that override to find the right
1103
+ * state.db; without it the reader sees ~/.hermes/state.db and misses
1104
+ * every coordinator-session transcript.
1105
+ *
1106
+ * Returns undefined when no SpecCliAdapter is in play (legacy
1107
+ * providers / CDP) or when the adapter exposes no spawn env.
1108
+ */
1109
+ function sessionSpawnEnvFromAdapter(h: CommandHelpers, targetSessionId: string | undefined): Record<string, string> | undefined {
1110
+ const adapter = getTargetedCliAdapter(h, { targetSessionId }, undefined);
1111
+ if (!adapter || typeof adapter.getRuntimeMetadata !== 'function') return undefined;
1112
+ const meta = adapter.getRuntimeMetadata() as Record<string, unknown> | undefined;
1113
+ const env = meta && typeof meta === 'object' ? (meta as Record<string, unknown>).spawnedEnv : undefined;
1114
+ return env && typeof env === 'object' ? env as Record<string, string> : undefined;
1115
+ }
1116
+
1117
+
1006
1118
  function readCliProviderNativeHistory(agentStr: string, args: {
1007
1119
  canonicalHistory?: ProviderModule['canonicalHistory'];
1008
1120
  historySessionId?: string;
@@ -1013,6 +1125,8 @@ function readCliProviderNativeHistory(agentStr: string, args: {
1013
1125
  historyBehavior?: ProviderModule['historyBehavior'];
1014
1126
  scripts?: ProviderScripts;
1015
1127
  excludeInProgressTurn?: boolean;
1128
+ sessionStartedAtMs?: number;
1129
+ envOverrides?: Record<string, string>;
1016
1130
  }): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
1017
1131
  if (!args.historySessionId) {
1018
1132
  return {
@@ -1033,6 +1147,8 @@ function readCliProviderNativeHistory(agentStr: string, args: {
1033
1147
  historyBehavior: args.historyBehavior,
1034
1148
  scripts: args.scripts as any,
1035
1149
  excludeInProgressTurn: args.excludeInProgressTurn,
1150
+ sessionStartedAtMs: args.sessionStartedAtMs,
1151
+ envOverrides: args.envOverrides,
1036
1152
  });
1037
1153
  // Native transcripts are keyed by provider/runtime session identity. Falling
1038
1154
  // back to workspace makes concurrent local Codex/Hermes sessions alias each
@@ -1290,7 +1406,7 @@ function collapseAdjacentDuplicateChatMessages(messages: ChatMessage[]): ChatMes
1290
1406
  return result;
1291
1407
  }
1292
1408
 
1293
- function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
1409
+ function buildReadChatCommandResult(payload: Record<string, any>, args: any, h?: CommandHelpers): CommandResult {
1294
1410
  let validatedPayload: Record<string, any>;
1295
1411
  const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
1296
1412
  ? payload.debugReadChat
@@ -1304,7 +1420,23 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
1304
1420
  return { success: false, error: error?.message || String(error) };
1305
1421
  }
1306
1422
  const messages = normalizeReadChatMessages(validatedPayload);
1307
- const visibleMessages = filterUserFacingChatMessages(messages);
1423
+ // Last-mile coordinator-prompt filter. Different read_chat code paths
1424
+ // produce the final messages array (native-history main path, codex
1425
+ // exact-runtime-mirror fallback, daemon-side pty-parser, etc), so
1426
+ // applying it here means we don't have to thread the filter through
1427
+ // every one. Driven by the provider setting `showCoordinatorSystemPrompt`
1428
+ // + the coordinator-registry entry for the target session.
1429
+ const sessionIdHint = typeof args?.targetSessionId === 'string' ? args.targetSessionId
1430
+ : typeof args?.sessionId === 'string' ? args.sessionId
1431
+ : '';
1432
+ const providerHint = typeof args?.cliType === 'string' ? args.cliType
1433
+ : typeof args?.providerType === 'string' ? args.providerType
1434
+ : typeof args?.agentType === 'string' ? args.agentType
1435
+ : '';
1436
+ const filteredMessages = h
1437
+ ? maybeHideCoordinatorPromptMessage(h, providerHint, sessionIdHint, messages)
1438
+ : messages;
1439
+ const visibleMessages = filterUserFacingChatMessages(filteredMessages);
1308
1440
  const sync = buildFullTail(visibleMessages, normalizeReadChatTailLimit(args));
1309
1441
  const hiddenMsgCount = Math.max(0, messages.length - visibleMessages.length);
1310
1442
  const preservedPayloadFields = Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key)));
@@ -1819,6 +1951,8 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
1819
1951
  excludeRecentCount,
1820
1952
  historyBehavior: provider?.historyBehavior,
1821
1953
  scripts: provider?.scripts as any,
1954
+ sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
1955
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1822
1956
  })
1823
1957
  : readProviderChatHistory(agentStr, {
1824
1958
  canonicalHistory: provider?.nativeHistory,
@@ -1833,7 +1967,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
1833
1967
  if (supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)) {
1834
1968
  const lookup = (result as any).lookup === 'workspace' ? 'workspace' : 'session';
1835
1969
  const messages = Array.isArray((result as any).messages)
1836
- ? normalizeNativeHistoryMessages(agentStr, (result as any).messages as ChatMessage[], (result as any)?.providerSessionId)
1970
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, (result as any).messages as ChatMessage[], (result as any)?.providerSessionId)
1837
1971
  : [];
1838
1972
  const historyProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
1839
1973
  ? (result as any).providerSessionId
@@ -2004,6 +2138,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2004
2138
  historyBehavior: provider?.historyBehavior,
2005
2139
  scripts: provider?.scripts as any,
2006
2140
  excludeInProgressTurn: returnedStatus === 'waiting_approval',
2141
+ sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
2142
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2007
2143
  });
2008
2144
  } catch (error: any) {
2009
2145
  nativeHistoryError = error;
@@ -2014,7 +2150,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2014
2150
  // 2. Compute safeMapping with the same rules the v1 code used so the
2015
2151
  // machine sees the same observation it always would have.
2016
2152
  const nativeMessages: ChatMessage[] = nativeHistory && Array.isArray(nativeHistory.messages)
2017
- ? normalizeNativeHistoryMessages(agentStr, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
2153
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
2018
2154
  : [];
2019
2155
  const historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
2020
2156
  ? nativeHistory.providerSessionId
@@ -2099,7 +2235,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2099
2235
  })
2100
2236
  : null;
2101
2237
  const liveWorkspaceNativeMessages = Array.isArray((liveWorkspaceNativeHistory as any)?.messages)
2102
- ? normalizeNativeHistoryMessages(agentStr, (liveWorkspaceNativeHistory as any).messages as ChatMessage[], (liveWorkspaceNativeHistory as any)?.providerSessionId)
2238
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, (liveWorkspaceNativeHistory as any).messages as ChatMessage[], (liveWorkspaceNativeHistory as any)?.providerSessionId)
2103
2239
  : [];
2104
2240
  const liveWorkspaceNativeProviderSessionId = typeof (liveWorkspaceNativeHistory as any)?.providerSessionId === 'string'
2105
2241
  ? (liveWorkspaceNativeHistory as any).providerSessionId
@@ -2214,7 +2350,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2214
2350
  ...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
2215
2351
  ...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
2216
2352
  ...(selectedCoverage ? { coverage: selectedCoverage } : {}),
2217
- }, args);
2353
+ }, args, h);
2218
2354
  }
2219
2355
  // History-only path (no adapter). Same source-decision contract as
2220
2356
  // the adapter path above, but with no PTY messages — the machine
@@ -2240,6 +2376,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2240
2376
  excludeRecentCount: 0,
2241
2377
  historyBehavior: provider?.historyBehavior,
2242
2378
  scripts: provider?.scripts as any,
2379
+ sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
2380
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2243
2381
  })
2244
2382
  : readProviderChatHistory(agentStr, {
2245
2383
  canonicalHistory: provider?.nativeHistory,
@@ -2253,7 +2391,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2253
2391
  });
2254
2392
  const lookup = (history as any)?.lookup === 'workspace' ? 'workspace' : 'session';
2255
2393
  const historyMessages = Array.isArray((history as any)?.messages)
2256
- ? normalizeNativeHistoryMessages(agentStr, (history as any).messages as ChatMessage[], (history as any)?.providerSessionId)
2394
+ ? normalizeAndFilterNativeHistory(h, agentStr, args, (history as any).messages as ChatMessage[], (history as any)?.providerSessionId)
2257
2395
  : [];
2258
2396
  const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
2259
2397
  ? (history as any).providerSessionId
@@ -2307,7 +2445,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2307
2445
  ? { transcriptAuthority: (provider?.historyBehavior as any).transcriptAuthority }
2308
2446
  : {}),
2309
2447
  coverage: 'tail',
2310
- }, args);
2448
+ }, args, h);
2311
2449
  } catch (error: any) {
2312
2450
  return { success: false, error: error?.message || `${transport} adapter not found` };
2313
2451
  }
@@ -2347,7 +2485,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2347
2485
  args?.targetSessionId,
2348
2486
  historySessionId,
2349
2487
  );
2350
- return buildReadChatCommandResult(validated as Record<string, any>, args);
2488
+ return buildReadChatCommandResult(validated as Record<string, any>, args, h);
2351
2489
  }
2352
2490
  if (!extensionReadChatError) {
2353
2491
  extensionReadChatError = 'extension read_chat returned a non-object payload';
@@ -2386,7 +2524,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2386
2524
  messages: stream.messages || [],
2387
2525
  status: stream.status,
2388
2526
  agentType: stream.agentType,
2389
- }, args);
2527
+ }, args, h);
2390
2528
  }
2391
2529
  }
2392
2530
  }
@@ -2426,7 +2564,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2426
2564
  args?.targetSessionId,
2427
2565
  historySessionId,
2428
2566
  );
2429
- return buildReadChatCommandResult(validated as Record<string, any>, args);
2567
+ return buildReadChatCommandResult(validated as Record<string, any>, args, h);
2430
2568
  }
2431
2569
  if (!webviewReadChatError) {
2432
2570
  webviewReadChatError = 'webview read_chat returned a non-object payload';
@@ -2476,7 +2614,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2476
2614
  args?.targetSessionId,
2477
2615
  historySessionId,
2478
2616
  );
2479
- return buildReadChatCommandResult(validated as Record<string, any>, args);
2617
+ return buildReadChatCommandResult(validated as Record<string, any>, args, h);
2480
2618
  }
2481
2619
  if (!ideReadChatError) {
2482
2620
  ideReadChatError = 'ide read_chat returned a non-object payload';
@@ -638,6 +638,17 @@ export class DaemonCliManager {
638
638
  transport: 'pty',
639
639
  adapterKey: key,
640
640
  instanceKey: key,
641
+ // attachExisting === true means we're restoring an already-spawned
642
+ // hosted runtime after a daemon restart, not starting a fresh PTY.
643
+ // The real spawn time is in the past and we don't have it on the
644
+ // restored descriptor; pinning spawnedAtMs to Date.now() in that
645
+ // case would push the native-history session-floor cutoff past
646
+ // every existing transcript file, so the agy/hermes/claude reader
647
+ // would return null even though the transcript on disk is fresh.
648
+ // 0 disables the floor for this session — recent_window_ms in the
649
+ // spec still bounds how far back we look. Fresh launches still
650
+ // get a proper floor so prior-session leak protection holds.
651
+ spawnedAtMs: attachExisting ? 0 : Date.now(),
641
652
  });
642
653
  } catch (spawnErr: any) {
643
654
  LOG.error('CLI', `[${cliType}] Spawn failed: ${spawnErr?.message}`);