@adhdev/mesh-shared 1.0.82-rc.4 → 1.0.82-rc.40

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 (65) hide show
  1. package/dist/assistant-protocol.d.ts +161 -6
  2. package/dist/decision-push-relay.d.ts +20 -0
  3. package/dist/index.d.ts +11 -0
  4. package/dist/index.js +1197 -118
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1107 -118
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/launch-defaults.d.ts +196 -0
  9. package/dist/mesh-tool-names.d.ts +2 -2
  10. package/dist/mesh-vocabulary.d.ts +13 -4
  11. package/dist/mission-git-state.d.ts +83 -0
  12. package/dist/mission-items.d.ts +202 -0
  13. package/dist/node-facts.d.ts +75 -1
  14. package/dist/outbound-message.d.ts +9 -2
  15. package/dist/path-ownership.d.ts +1 -1
  16. package/dist/protocol/daemon-server.d.ts +42 -2
  17. package/dist/protocol/dashboard-daemon-p2p.d.ts +1 -1
  18. package/dist/protocol/dashboard-server.d.ts +20 -2
  19. package/dist/provider-approval.d.ts +36 -0
  20. package/dist/provider-question.d.ts +19 -0
  21. package/dist/quota-freshness.d.ts +65 -0
  22. package/dist/relay-reprobe.d.ts +52 -0
  23. package/dist/release.d.ts +90 -0
  24. package/dist/role-launch.d.ts +11 -0
  25. package/dist/session-launch.d.ts +10 -22
  26. package/dist/slot-proposal.d.ts +14 -0
  27. package/dist/task-source-phase.d.ts +75 -0
  28. package/dist/turn-evidence.d.ts +29 -13
  29. package/dist/turn-ipc-mission.d.ts +68 -1
  30. package/dist/turn-ipc-queue.d.ts +19 -0
  31. package/dist/turn-ipc.d.ts +2 -2
  32. package/dist/types.d.ts +1 -0
  33. package/dist/worker-protocol.d.ts +1 -1
  34. package/package.json +1 -1
  35. package/src/assistant-protocol.ts +302 -6
  36. package/src/daemon-normalize.ts +7 -5
  37. package/src/decision-push-relay.ts +23 -0
  38. package/src/index.ts +12 -0
  39. package/src/keyed-doc-delta.ts +9 -1
  40. package/src/launch-defaults.ts +170 -0
  41. package/src/mesh-tool-names.ts +16 -0
  42. package/src/mesh-vocabulary.ts +12 -2
  43. package/src/mission-git-state.ts +112 -0
  44. package/src/mission-items.ts +421 -0
  45. package/src/node-facts.ts +70 -1
  46. package/src/outbound-message.ts +19 -0
  47. package/src/path-ownership.ts +4 -6
  48. package/src/protocol/daemon-server.ts +37 -2
  49. package/src/protocol/dashboard-server.ts +13 -2
  50. package/src/provider-approval.ts +59 -0
  51. package/src/provider-question.ts +31 -0
  52. package/src/quota-freshness.ts +139 -0
  53. package/src/relay-reprobe.ts +117 -0
  54. package/src/release.ts +121 -0
  55. package/src/role-launch.ts +51 -0
  56. package/src/session-launch.ts +17 -1
  57. package/src/slot-proposal.ts +61 -2
  58. package/src/task-source-phase.ts +61 -0
  59. package/src/turn-evidence.ts +27 -20
  60. package/src/turn-ipc-mission.ts +121 -11
  61. package/src/turn-ipc-queue.ts +29 -1
  62. package/src/turn-ipc-turn.ts +1 -1
  63. package/src/turn-ipc.ts +1 -0
  64. package/src/types.ts +1 -0
  65. package/src/worker-protocol.ts +15 -0
@@ -11,22 +11,35 @@
11
11
  * - the daemon-only verbs (launch / MCP-only pull / owner actions) that have
12
12
  * no tool.
13
13
  *
14
- * Assistant tools are a separate set from the mesh tools: none of these names
15
- * belongs in `CANONICAL_MESH_TOOL_NAMES`, and the coordinator/worker MCP modes
16
- * never publish them.
14
+ * Assistant-specific tools remain separate from mesh tools. Driver discovery
15
+ * (list/describe) is shared across all three modes; invoke remains Worker-only.
17
16
  */
18
17
 
19
18
  export const ASSISTANT_TOOLS = [
20
19
  'projects',
20
+ 'environment',
21
+ 'assistant_action',
22
+ 'project_watch',
21
23
  'project_status',
22
24
  'project_send',
23
25
  'project_read',
24
26
  'project_add',
27
+ 'project_attach_remote',
25
28
  'discover_repos',
26
29
  'memory',
30
+ 'memory_search',
27
31
  'skill_view',
28
32
  'skill_manage',
29
33
  'project_note',
34
+ 'launch_defaults',
35
+ 'launch_defaults_set',
36
+ 'driver_list',
37
+ 'driver_describe',
38
+ 'artifact_list',
39
+ 'artifact_read',
40
+ 'decision_answer',
41
+ 'decision_request',
42
+ 'session_control',
30
43
  ] as const
31
44
 
32
45
  export type AssistantTool = typeof ASSISTANT_TOOLS[number]
@@ -40,16 +53,36 @@ export function isAssistantTool(value: unknown): value is AssistantTool {
40
53
  /** Daemon verb names (§4.4 table + the five verbs below it). */
41
54
  export const ASSISTANT_VERB = {
42
55
  // tool verbs
56
+ driverList: 'driver_list',
57
+ driverDescribe: 'driver_describe',
43
58
  projects: 'assistant_projects',
59
+ environment: 'assistant_environment',
60
+ action: 'assistant_action',
61
+ watch: 'assistant_project_watch',
62
+ executionState: 'assistant_execution_state',
63
+ executionSettings: 'assistant_execution_settings',
44
64
  projectStatus: 'assistant_project_status',
45
65
  projectSend: 'assistant_project_send',
46
66
  projectRead: 'assistant_project_read',
47
67
  projectAdd: 'assistant_project_add',
68
+ projectAttachRemote: 'assistant_project_attach_remote',
48
69
  discoverRepos: 'assistant_discover_repos',
49
70
  memory: 'assistant_memory',
71
+ /** Pull-only FTS search over the archive tier (memory design 2026-11-01 §3.4). */
72
+ memorySearch: 'assistant_memory_search',
50
73
  skillView: 'assistant_skill_view',
51
74
  skillManage: 'assistant_skill_manage',
52
75
  projectNote: 'assistant_project_note',
76
+ launchDefaults: 'assistant_launch_defaults',
77
+ launchDefaultsSet: 'assistant_launch_defaults_set',
78
+ artifactList: 'assistant_artifact_list',
79
+ artifactRead: 'assistant_artifact_read',
80
+ /** Owner-decision inbox (structured-missions design 2026-10-28 §6.3): the owner's words recorded as answers. */
81
+ decisionAnswer: 'assistant_decision_answer',
82
+ /** Register a plain-text question a coordinator put to the owner as a decision item (§5.2). */
83
+ decisionRequest: 'assistant_decision_request',
84
+ /** Stop / restart / switch the provider of a coordinator, worker or the assistant session (handoff design 2026-11-01 §4). */
85
+ sessionControl: 'assistant_session_control',
53
86
  // assistant-side daemon verbs without a tool of the same name
54
87
  launch: 'launch_assistant',
55
88
  pendingRelays: 'assistant_pending_relays',
@@ -57,22 +90,45 @@ export const ASSISTANT_VERB = {
57
90
  stagedResolve: 'assistant_staged_resolve',
58
91
  storeAdmin: 'assistant_store_admin',
59
92
  importSkills: 'assistant_import_skills',
93
+ /** Owner skill list (attach/view counts, save proposals, recent skill metrics); reads move no counter. */
94
+ skillsList: 'assistant_skills_list',
95
+ /** Owner: what the assistant remembered recently, with undo handles (memory design §3.2). */
96
+ memoryDigest: 'assistant_memory_digest',
97
+ /** Owner: reverse one remembered write or one batch (memory design §3.2). Never an assistant tool. */
98
+ memoryUndo: 'assistant_memory_undo',
99
+ /** Projects the assistant is following (open relay threads) — list / stop following. */
100
+ threads: 'assistant_threads',
101
+ threadClose: 'assistant_thread_close',
60
102
  } as const
61
103
 
62
104
  export type AssistantVerb = typeof ASSISTANT_VERB[keyof typeof ASSISTANT_VERB]
63
105
 
64
106
  /** The daemon verb each assistant tool calls. */
65
107
  export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>> = {
108
+ driver_list: ASSISTANT_VERB.driverList,
109
+ driver_describe: ASSISTANT_VERB.driverDescribe,
66
110
  projects: ASSISTANT_VERB.projects,
111
+ environment: ASSISTANT_VERB.environment,
112
+ assistant_action: ASSISTANT_VERB.action,
113
+ project_watch: ASSISTANT_VERB.watch,
67
114
  project_status: ASSISTANT_VERB.projectStatus,
68
115
  project_send: ASSISTANT_VERB.projectSend,
69
116
  project_read: ASSISTANT_VERB.projectRead,
70
117
  project_add: ASSISTANT_VERB.projectAdd,
118
+ project_attach_remote: ASSISTANT_VERB.projectAttachRemote,
71
119
  discover_repos: ASSISTANT_VERB.discoverRepos,
72
120
  memory: ASSISTANT_VERB.memory,
121
+ memory_search: ASSISTANT_VERB.memorySearch,
73
122
  skill_view: ASSISTANT_VERB.skillView,
74
123
  skill_manage: ASSISTANT_VERB.skillManage,
75
124
  project_note: ASSISTANT_VERB.projectNote,
125
+ launch_defaults: ASSISTANT_VERB.launchDefaults,
126
+ launch_defaults_set: ASSISTANT_VERB.launchDefaultsSet,
127
+ artifact_list: ASSISTANT_VERB.artifactList,
128
+ artifact_read: ASSISTANT_VERB.artifactRead,
129
+ decision_answer: ASSISTANT_VERB.decisionAnswer,
130
+ decision_request: ASSISTANT_VERB.decisionRequest,
131
+ session_control: ASSISTANT_VERB.sessionControl,
76
132
  }
77
133
 
78
134
  /**
@@ -81,25 +137,32 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
81
137
  * because they cause effects (a coordinator message or launch, a new
82
138
  * project), and MCP clients use readOnlyHint to skip confirmation.
83
139
  */
84
- export const ASSISTANT_WRITE_TOOLS: readonly AssistantTool[] = ['memory', 'skill_manage', 'project_note', 'project_send', 'project_add']
140
+ export const ASSISTANT_WRITE_TOOLS: readonly AssistantTool[] = ['project_watch', 'assistant_action', 'memory', 'skill_manage', 'project_note', 'project_send', 'project_add', 'project_attach_remote', 'launch_defaults_set', 'decision_answer', 'decision_request', 'session_control']
85
141
 
86
142
  /** Owner-only verbs: never reachable from the assistant's MCP server (no `ipc`). */
87
143
  export const ASSISTANT_OWNER_VERBS: readonly AssistantVerb[] = [
144
+ ASSISTANT_VERB.executionSettings,
88
145
  ASSISTANT_VERB.stagedResolve,
89
146
  ASSISTANT_VERB.storeAdmin,
90
147
  ASSISTANT_VERB.importSkills,
148
+ ASSISTANT_VERB.skillsList,
149
+ ASSISTANT_VERB.memoryDigest,
150
+ ASSISTANT_VERB.memoryUndo,
151
+ ASSISTANT_VERB.threadClose,
91
152
  ]
92
153
 
93
154
  /** The only assistant verbs accepted while an idle review turn is open (§4.10.7). */
94
155
  export const ASSISTANT_REVIEW_TURN_VERBS: readonly AssistantVerb[] = [
95
156
  ASSISTANT_VERB.memory,
157
+ ASSISTANT_VERB.memorySearch,
96
158
  ASSISTANT_VERB.skillView,
97
159
  ASSISTANT_VERB.skillManage,
98
160
  ASSISTANT_VERB.projectNote,
99
161
  ]
100
162
 
101
- /**
102
- * Env var the daemon sets on the assistant CLI's process (§4.5), read by the
163
+ import { canonicalDaemonId } from './daemon-normalize'
164
+
165
+ /** Env var the daemon sets on the assistant CLI's process (§4.5), read by the
103
166
  * assistant MCP server and forwarded on every verb as
104
167
  * `ASSISTANT_SESSION_ID_ARG`. Routing / ownership only — never an auth gate.
105
168
  */
@@ -107,3 +170,236 @@ export const ASSISTANT_SESSION_ID_ENV = 'ADHDEV_ASSISTANT_SESSION_ID'
107
170
 
108
171
  /** Request arg carrying the calling assistant session id. */
109
172
  export const ASSISTANT_SESSION_ID_ARG = 'assistantSessionId'
173
+
174
+ // ─── Remote Assistant Project Follow & Host-Authoritative Grant (rc24) ──────
175
+
176
+ /**
177
+ * Conceptual single-assistant slot in the verified account namespace.
178
+ * Fixed to `'personal'` — one logical assistant per account with one
179
+ * owner-selected active execution daemon binding per project ACL.
180
+ */
181
+ export const ASSISTANT_ACCOUNT_SLOT = 'personal' as const
182
+ export type AssistantAccountSlot = typeof ASSISTANT_ACCOUNT_SLOT
183
+
184
+ /**
185
+ * Daemon commands for remote project discovery, host ACL grant/revoke, and
186
+ * assistant-side follow/unfollow. Registered outside `ASSISTANT_VERB` so the
187
+ * assistant MCP server tool/verb parity table remains untouched.
188
+ */
189
+ export const ASSISTANT_REMOTE_PROJECT_COMMAND = 'assistant_remote_project' as const
190
+ export const ASSISTANT_REMOTE_PROJECT_CATALOG_COMMAND = 'assistant_remote_project_catalog' as const
191
+ export const ASSISTANT_REMOTE_PROJECT_ACCESS_COMMAND = 'assistant_remote_project_access' as const
192
+ export const ASSISTANT_REMOTE_FOLLOW_COMMAND = 'assistant_remote_follow' as const
193
+
194
+ /**
195
+ * Narrow operations a non-roster follower deputy is permitted to invoke on
196
+ * `assistant_remote_project` when the host's `assistantRemoteAccess` ACL is active.
197
+ * `note`, `terminal`, and `inventory` remain strictly roster-member-only.
198
+ */
199
+ export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send', 'artifact_list', 'artifact_read', 'decision_list', 'decision_answer', 'decision_request'] as const
200
+ export type AssistantFollowerAllowedOp = typeof ASSISTANT_FOLLOWER_ALLOWED_OPS[number]
201
+
202
+ const ASSISTANT_FOLLOWER_ALLOWED_OP_SET: ReadonlySet<string> = new Set(ASSISTANT_FOLLOWER_ALLOWED_OPS)
203
+
204
+ export function isAssistantFollowerAllowedOp(value: unknown): value is AssistantFollowerAllowedOp {
205
+ return typeof value === 'string' && ASSISTANT_FOLLOWER_ALLOWED_OP_SET.has(value)
206
+ }
207
+
208
+ /**
209
+ * Owner-decision ops were added after grants were persisted with an explicit
210
+ * `allowedOps` list. A grant that already lets the follower read status (list)
211
+ * or send to the coordinator (answer / register) covers the matching decision op,
212
+ * so no existing grant needs re-issuing.
213
+ */
214
+ const DECISION_OP_IMPLIED_BY: Readonly<Record<string, AssistantFollowerAllowedOp>> = {
215
+ decision_list: 'status',
216
+ decision_answer: 'send',
217
+ decision_request: 'send',
218
+ }
219
+
220
+ export function followerOpPermitted(op: string, allowedOps: readonly AssistantFollowerAllowedOp[]): boolean {
221
+ if (!isAssistantFollowerAllowedOp(op)) return false
222
+ if ((allowedOps as readonly string[]).includes(op)) return true
223
+ const implied = DECISION_OP_IMPLIED_BY[op]
224
+ return implied !== undefined && allowedOps.includes(implied)
225
+ }
226
+
227
+ /**
228
+ * Host-authoritative narrow project ACL persisted on the hosting daemon's
229
+ * `LocalMeshEntry` in `meshes.json`. Revocation increments `grantRevision`
230
+ * and retains a `'revoked'` tombstone so stale follower caches cannot replay.
231
+ */
232
+ export interface AssistantRemoteAccessBinding {
233
+ hostDaemonId: string
234
+ meshId: string
235
+ accountSlot: AssistantAccountSlot
236
+ activeDaemonId: string
237
+ bindingRevision: number
238
+ grantRevision: number
239
+ status: 'active' | 'revoked'
240
+ allowedOps: readonly AssistantFollowerAllowedOp[]
241
+ allowCoordinatorLaunch?: boolean
242
+ allowArtifactRead?: boolean
243
+ grantedAt: number
244
+ revokedAt?: number
245
+ }
246
+
247
+ /**
248
+ * Durable owner-binding receipt persisted in the assistant daemon's
249
+ * `assistant.json`. Survives ordinary daemon restarts and session/provider
250
+ * replacements (`bindSession`), while missing/corrupt/mismatched receipts
251
+ * fail closed until owner reconciliation.
252
+ */
253
+ export interface AssistantOwnerBindingReceipt {
254
+ accountSlot: AssistantAccountSlot
255
+ daemonId: string
256
+ bindingRevision: number
257
+ boundAt: number | string
258
+ }
259
+
260
+ export type FollowedRemoteProjectState = 'followed' | 'unfollowed' | 'revoked'
261
+
262
+ export type RemoteAccessState =
263
+ | 'not_granted'
264
+ | 'granted_unfollowed'
265
+ | 'followed'
266
+ | 'revoked'
267
+ | 'host_unreachable'
268
+ | 'binding_mismatch'
269
+
270
+ /**
271
+ * Assistant-local followed remote project reference persisted in `assistant.json`.
272
+ * Never injected into `meshes.json`, `mesh-host-records.json`, `mesh-node-state-push.json`,
273
+ * or `inlineMeshCache`.
274
+ */
275
+ export interface FollowedRemoteProjectEntry {
276
+ /** Canonical key: `${canonicalDaemonId(hostDaemonId)}::${meshId}` */
277
+ key: string
278
+ hostDaemonId: string
279
+ hostLabel?: string
280
+ meshId: string
281
+ name: string
282
+ slug: string
283
+ sanitizedRepoIdentity: string | null
284
+ repoIdentity?: string
285
+ accountSlot: AssistantAccountSlot
286
+ bindingRevision: number
287
+ grantRevision: number
288
+ allowCoordinatorLaunch?: boolean
289
+ allowArtifactRead?: boolean
290
+ state: FollowedRemoteProjectState
291
+ followedAt: number | string
292
+ updatedAt: number | string
293
+ unfollowedAt?: number | string
294
+ revokedAt?: number | string
295
+ revokedReason?: string
296
+ lastCursor?: string | null
297
+ coordinatorSessionId?: string | null
298
+ lastErrorCode?: string
299
+ lastObserved?: {
300
+ observedAt: number | string
301
+ coordinatorState?: string
302
+ coordinatorMissing?: boolean
303
+ coordinatorPresent?: boolean
304
+ lastHeadline?: string
305
+ summary?: string
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Content-free canonical catalog metadata returned by `assistant_remote_project_catalog`
311
+ * for meshes actually hosted on the queried remote daemon.
312
+ */
313
+ export interface RemoteProjectCatalogItem {
314
+ hostDaemonId: string
315
+ meshId: string
316
+ name: string
317
+ sanitizedRepoIdentity: string | null
318
+ coordinatorState?: 'none' | 'idle' | 'working' | 'waiting' | string
319
+ allowCoordinatorLaunch?: boolean
320
+ allowArtifactRead?: boolean
321
+ capabilities: {
322
+ status: true
323
+ read: true
324
+ poll: true
325
+ report: true
326
+ send: true
327
+ coordinatorPresent?: boolean
328
+ allowCoordinatorLaunch?: boolean
329
+ allowArtifactRead?: boolean
330
+ }
331
+ observedAt: number | string
332
+ }
333
+
334
+ /** Build the canonical `(hostDaemonId, meshId)` key used across assistant follow state. */
335
+ export function followedRemoteProjectKey(hostDaemonId: string, meshId: string): string {
336
+ const canonHost = canonicalDaemonId(hostDaemonId) ?? hostDaemonId.trim()
337
+ return `${canonHost}::${meshId.trim()}`
338
+ }
339
+
340
+ /**
341
+ * Sanitize a repository identity or remote URL into a content-free `host/path`
342
+ * identifier suitable for remote discovery catalogs. Strips credentials, query
343
+ * strings, fragments, and `.git` suffixes, and rejects local filesystem paths.
344
+ */
345
+ export function sanitizeRemoteRepoIdentity(raw: string | null | undefined): string | null {
346
+ if (typeof raw !== 'string') return null
347
+ const trimmed = raw.trim()
348
+ if (!trimmed) return null
349
+
350
+ // Reject local filesystem paths (Unix absolute/relative/home, Windows drive/UNC, file:// URLs).
351
+ if (
352
+ trimmed.startsWith('/')
353
+ || trimmed.startsWith('./')
354
+ || trimmed.startsWith('../')
355
+ || trimmed.startsWith('~/')
356
+ || trimmed.startsWith('\\')
357
+ || /^[a-zA-Z]:[\\/]/.test(trimmed)
358
+ || /^file:/i.test(trimmed)
359
+ ) {
360
+ return null
361
+ }
362
+
363
+ const withoutQuery = trimmed.replace(/[?#].*$/, '').replace(/\/+$/, '')
364
+ if (!withoutQuery) return null
365
+
366
+ let host = ''
367
+ let repoPath = ''
368
+
369
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(withoutQuery)) {
370
+ try {
371
+ const url = new URL(withoutQuery)
372
+ if (url.protocol === 'file:') return null
373
+ host = url.hostname.toLowerCase()
374
+ repoPath = decodeURIComponent(url.pathname)
375
+ .replace(/^\/+|\/+$/g, '')
376
+ .replace(/\.git$/i, '')
377
+ } catch {
378
+ return null
379
+ }
380
+ } else {
381
+ const scpMatch = withoutQuery.match(/^(?:[^@/:]+@)?(\[[^\]]+\]|[^/:]+):(.+)$/)
382
+ if (scpMatch) {
383
+ host = scpMatch[1].replace(/^\[|\]$/g, '').toLowerCase()
384
+ repoPath = scpMatch[2].replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '')
385
+ } else {
386
+ const slash = withoutQuery.indexOf('/')
387
+ if (slash > 0) {
388
+ const rawHost = withoutQuery.slice(0, slash)
389
+ const atIdx = rawHost.lastIndexOf('@')
390
+ host = (atIdx >= 0 ? rawHost.slice(atIdx + 1) : rawHost).toLowerCase()
391
+ repoPath = withoutQuery.slice(slash + 1).replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '')
392
+ }
393
+ }
394
+ }
395
+
396
+ if (!host || !repoPath) return null
397
+ if (!host.includes('.') && host !== 'localhost') return null
398
+ if (/[\s@:\\]/.test(host)) return null
399
+ const segments = repoPath.split('/').filter(Boolean)
400
+ if (segments.length === 0) return null
401
+ if (segments.some(seg => seg === '.' || seg === '..' || /[\s@:\\]/.test(seg))) return null
402
+
403
+ return `${host}/${segments.join('/')}`.slice(0, 200)
404
+ }
405
+
@@ -108,13 +108,15 @@ export function isPhantomDaemonEntry(
108
108
  export function machineCoreFromDaemonId(id: string | null | undefined): string | undefined {
109
109
  const trimmed = readString(id)
110
110
  if (!trimmed) return undefined
111
+ const lower = trimmed.toLowerCase()
111
112
  for (const prefix of DAEMON_ID_PREFIXES) {
112
- if (trimmed.startsWith(prefix)) {
113
+ if (lower.startsWith(prefix)) {
113
114
  const core = trimmed.slice(prefix.length).trim()
114
- return core || undefined
115
+ if (!core) return undefined
116
+ return /^mach_/i.test(core) ? `mach_${core.slice(5)}` : core
115
117
  }
116
118
  }
117
- return trimmed
119
+ return /^mach_/i.test(trimmed) ? `mach_${trimmed.slice(5)}` : trimmed
118
120
  }
119
121
 
120
122
  /**
@@ -144,8 +146,8 @@ export function machineCoreFromDaemonId(id: string | null | undefined): string |
144
146
  export function canonicalDaemonId(id: string | null | undefined): string | undefined {
145
147
  const core = machineCoreFromDaemonId(id)
146
148
  if (!core) return undefined
147
- if (!core.startsWith('mach_')) return core
148
- return `daemon_${core}`
149
+ if (!/^mach_/i.test(core)) return core
150
+ return `daemon_mach_${core.slice(5)}`
149
151
  }
150
152
 
151
153
  /** True when both ids resolve to the same machine core — i.e. they are the same
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Opt-in "answer a decision from the push notification" relay (mission 4ae3edf2, owner 2026-10-29).
3
+ *
4
+ * ★ A deliberate, narrow exception to "no dashboard command relay over server WS" (CLAUDE.md).
5
+ * With the user's opt-in setting ON and no dashboard window open, the notification's
6
+ * recommended-option action is POSTed by the service worker to the server, which forwards it to the
7
+ * owning daemon over the existing daemon WS as ONE fixed command. What crosses the server is exactly
8
+ * `{ meshId, itemId, optionId }` (+ the single-use answer token that authorizes it) — never the
9
+ * question, context or free text. Shared so the server's frame and the daemon's ingress cannot drift.
10
+ */
11
+
12
+ /** Daemon command the server sends; its only accepted source is {@link DECISION_PUSH_RELAY_SOURCE}. */
13
+ export const DECISION_PUSH_RELAY_COMMAND = 'mission_decision_push_relay_answer'
14
+
15
+ /** Command source the server stamps on the relayed frame (never settable by REST/API callers). */
16
+ export const DECISION_PUSH_RELAY_SOURCE = 'push_relay'
17
+
18
+ /** The complete relayed payload — ids only. */
19
+ export type DecisionPushRelayPayload = {
20
+ meshId: string
21
+ itemId: string
22
+ optionId: string
23
+ }
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export * from './git-normalize'
16
16
  export * from './session-normalize'
17
17
  export * from './node-normalize'
18
18
  export * from './node-facts'
19
+ export * from './quota-freshness'
19
20
  export * from './workspace-normalize'
20
21
  export * from './daemon-normalize'
21
22
  export * from './git-summarize'
@@ -40,4 +41,15 @@ export * from './session-launch'
40
41
  export * from './turn-ipc'
41
42
  export * from './path-ownership'
42
43
  export * from './mission-brief'
44
+ export * from './mission-items'
45
+ export * from './mission-git-state'
46
+ export * from './release'
47
+ export * from './decision-push-relay'
43
48
  export * from './keyed-doc-delta'
49
+ export * from './provider-approval'
50
+ export * from './provider-question'
51
+ export * from './launch-defaults'
52
+
53
+ export * from './role-launch';
54
+ export * from './task-source-phase';
55
+ export * from './relay-reprobe'
@@ -326,7 +326,15 @@ export function foldKeyedDoc<T extends Record<string, unknown>>(held: T, delta:
326
326
  const heldList = heldCollections.get(path)
327
327
  if (change?.absent) {
328
328
  const [head, tail] = splitPath(path)
329
- if (!tail) delete next[head]
329
+ if (!tail) {
330
+ delete next[head]
331
+ continue
332
+ }
333
+ const parent = next[head]
334
+ if (!isRecord(parent) || !(tail in parent)) continue
335
+ const copy = { ...parent }
336
+ delete copy[tail]
337
+ next[head] = copy
330
338
  continue
331
339
  }
332
340
  if (!change && heldList === undefined) continue
@@ -0,0 +1,170 @@
1
+ /** Daemon-local launch intent and command wire contract. P2P/local only;
2
+ * never include this document or its history in cloud status_report.
3
+ * Publication through mesh-shared's barrel is a later integration step. */
4
+ export const LAUNCH_DEFAULT_ROLES = ['assistant', 'coordinator'] as const;
5
+ export type LaunchDefaultRole = typeof LAUNCH_DEFAULT_ROLES[number];
6
+ export const LAUNCH_DEFAULT_SOURCES = ['user', 'stored_default', 'project_default', 'provider_default'] as const;
7
+ export type LaunchDefaultSource = typeof LAUNCH_DEFAULT_SOURCES[number];
8
+ export const LAUNCH_DEFAULTS_NEXT_LAUNCH_NOTE = 'Applies to the next launch; running sessions are unchanged.';
9
+ export function isLaunchDefaultRole(value: unknown): value is LaunchDefaultRole {
10
+ return typeof value === 'string' && (LAUNCH_DEFAULT_ROLES as readonly string[]).includes(value);
11
+ }
12
+ export function isLaunchDefaultSource(value: unknown): value is LaunchDefaultSource {
13
+ return typeof value === 'string' && (LAUNCH_DEFAULT_SOURCES as readonly string[]).includes(value);
14
+ }
15
+ export interface LaunchDefaultSelection {
16
+ provider: string;
17
+ /** Omission means pass no model/thinking argument, not inherit another provider. */
18
+ model?: string;
19
+ thinkingLevel?: string;
20
+ }
21
+ export type LaunchDefaultToolRestriction = 'enforced' | 'prompt_only';
22
+ export type LaunchDefaultOrigin = 'owner' | 'human' | 'relay' | 'review' | 'review_tainted';
23
+ export interface LaunchDefaultEntry extends LaunchDefaultSelection {
24
+ revision: number;
25
+ setBy: 'owner' | 'assistant';
26
+ setAt: number;
27
+ /** Ordered quota-failover alternatives (<= 3, distinct providers). Absent = none. */
28
+ fallback?: LaunchDefaultSelection[];
29
+ }
30
+ export interface LaunchDefaultChange {
31
+ at: number;
32
+ role: LaunchDefaultRole;
33
+ by: 'owner' | 'assistant';
34
+ origin: LaunchDefaultOrigin;
35
+ from: LaunchDefaultEntry | null;
36
+ to: LaunchDefaultEntry | null;
37
+ }
38
+ export interface LaunchDefaultsDocument {
39
+ version: 1;
40
+ /** Global document token, including clears; distinct from entry revisions. */
41
+ revision: number;
42
+ assistant?: LaunchDefaultEntry;
43
+ coordinator?: LaunchDefaultEntry;
44
+ history: LaunchDefaultChange[];
45
+ }
46
+ export interface LaunchDefaultRef {
47
+ role: LaunchDefaultRole;
48
+ scope: 'global' | 'project';
49
+ /** Entry revision for global defaults; host project token for project defaults. */
50
+ revision: number;
51
+ projectId?: string;
52
+ }
53
+ export type LaunchDefaultField = keyof LaunchDefaultSelection;
54
+ export interface LaunchDefaultAxis {
55
+ value: string | null;
56
+ source: LaunchDefaultSource;
57
+ defaultRef?: LaunchDefaultRef;
58
+ }
59
+ export type LaunchDefaultWarning = 'catalog_unverified' | 'manual_mcp_setup_required';
60
+ export type LaunchDefaultValidationReason =
61
+ | 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled'
62
+ | 'provider_not_detected' | 'provider_not_eligible' | 'provider_quota_gated' | 'model_unsupported'
63
+ | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported'
64
+ | 'thinking_not_offered' | 'catalog_unavailable';
65
+ export type LaunchDefaultResolution =
66
+ | { ok: true; selection: LaunchDefaultSelection; applied: Record<LaunchDefaultField, LaunchDefaultAxis>;
67
+ defaultRef?: LaunchDefaultRef; documentRevision: number; warnings: LaunchDefaultWarning[];
68
+ toolRestriction: LaunchDefaultToolRestriction }
69
+ | { ok: false; code: 'launch_default_required' | 'launch_default_unavailable' | 'launch_selection_invalid';
70
+ role: LaunchDefaultRole; field: LaunchDefaultField; reason: string; source?: LaunchDefaultSource;
71
+ value?: string | null; defaultRef?: LaunchDefaultRef; candidates?: readonly string[]; hint: string };
72
+ export interface LaunchDefaultsRoleView {
73
+ stored: LaunchDefaultEntry | null;
74
+ /** The existing daemon resolver's result, including loud failures. */
75
+ effective: LaunchDefaultResolution;
76
+ }
77
+ type Target = { role: 'assistant'; project?: never } | { role: 'coordinator'; project?: string };
78
+ export type LaunchDefaultsReadRequest =
79
+ | ({ action?: 'get' } & (Target | { role?: never; project?: never }))
80
+ | ({ action: 'check' } & Target & LaunchDefaultSelection);
81
+ /** Mandatory CAS: global document token, or host project token for project writes.
82
+ * 0 is explicit first setup; clears advance the token and never restore fallback. */
83
+ export type LaunchDefaultsSetRequest = Target & { expectedRevision: number } & (
84
+ | ({ action: 'set' } & LaunchDefaultSelection)
85
+ | { action: 'clear'; provider?: never; model?: never; thinkingLevel?: never }
86
+ );
87
+ /** Existing assistant routing envelope; session identity never supplies origin. */
88
+ export type AssistantLaunchDefaultsReadRequest = LaunchDefaultsReadRequest & { assistantSessionId?: string };
89
+ export type AssistantLaunchDefaultsSetRequest = LaunchDefaultsSetRequest & { assistantSessionId?: string };
90
+ export const LAUNCH_DEFAULT_ERROR_CODES = [
91
+ 'invalid_args', 'launch_default_required', 'launch_default_unavailable', 'launch_selection_invalid',
92
+ 'launch_default_invalid', 'revision_conflict', 'store_unavailable', 'store_busy', 'persistence_failed',
93
+ 'project_not_found', 'project_ambiguous', 'project_not_hosted', 'project_storage_unavailable',
94
+ 'staging_unavailable', 'staging_failed', 'review_turn_tool_denied',
95
+ ] as const;
96
+ export type LaunchDefaultsErrorCode = typeof LAUNCH_DEFAULT_ERROR_CODES[number];
97
+ export interface LaunchDefaultsError {
98
+ success: false;
99
+ code: LaunchDefaultsErrorCode;
100
+ error: string;
101
+ reason?: string;
102
+ role?: LaunchDefaultRole;
103
+ field?: LaunchDefaultField;
104
+ source?: LaunchDefaultSource;
105
+ value?: string | null;
106
+ defaultRef?: LaunchDefaultRef;
107
+ candidates?: readonly string[];
108
+ actualRevision?: number;
109
+ /** 0 for an absent global document; absent when it cannot be read. */
110
+ documentRevision?: number;
111
+ /** A persistence failure can replace intent without confirming durability.
112
+ * Re-read before retrying; never report applied on committed:true alone. */
113
+ committed?: boolean;
114
+ }
115
+ export interface LaunchDefaultsReadSuccess {
116
+ success: true;
117
+ result: 'get';
118
+ documentRevision: number;
119
+ roles: Partial<Record<LaunchDefaultRole, LaunchDefaultsRoleView>>;
120
+ project?: { projectId: string; revision: number; entry: LaunchDefaultEntry | null };
121
+ }
122
+ export interface LaunchDefaultsCheckSuccess {
123
+ success: true;
124
+ result: 'check';
125
+ role: LaunchDefaultRole;
126
+ selection: LaunchDefaultSelection;
127
+ warnings: LaunchDefaultWarning[];
128
+ toolRestriction: LaunchDefaultToolRestriction;
129
+ lowersToolLock: boolean;
130
+ }
131
+ export interface LaunchDefaultsApplied {
132
+ success: true;
133
+ result: 'applied';
134
+ role: LaunchDefaultRole;
135
+ scope: 'global' | 'project';
136
+ projectId?: string;
137
+ entry: LaunchDefaultEntry | null;
138
+ previous: LaunchDefaultEntry | null;
139
+ /** CAS token returned by the authority that performed the write. */
140
+ revision: number;
141
+ warnings: LaunchDefaultWarning[];
142
+ durable: true;
143
+ note: string;
144
+ }
145
+ export interface LaunchDefaultsStaged {
146
+ success: true;
147
+ result: 'staged';
148
+ role: LaunchDefaultRole;
149
+ stagedId: string;
150
+ previous: LaunchDefaultEntry | null;
151
+ selection: LaunchDefaultSelection | null;
152
+ expectedRevision: number;
153
+ warnings: LaunchDefaultWarning[];
154
+ note: string;
155
+ }
156
+ /** Existing owner Inbox adapter stores this proposal; it is not another default.
157
+ * Applying it must revalidate on the host and CAS via the same set handler. */
158
+ export interface LaunchDefaultsProposal {
159
+ kind: 'launch_defaults';
160
+ role: LaunchDefaultRole;
161
+ project?: string;
162
+ selection: LaunchDefaultSelection | null;
163
+ previous: LaunchDefaultEntry | null;
164
+ expectedRevision: number;
165
+ origin: LaunchDefaultOrigin;
166
+ callerSessionId?: string;
167
+ }
168
+ export type LaunchDefaultsReadResponse = LaunchDefaultsReadSuccess | LaunchDefaultsCheckSuccess | LaunchDefaultsError;
169
+ export type LaunchDefaultsSetResponse = LaunchDefaultsApplied | LaunchDefaultsStaged | LaunchDefaultsError;
170
+ export type LaunchDefaultsResponse = LaunchDefaultsReadResponse | LaunchDefaultsSetResponse;