@adhdev/mesh-shared 1.0.82-rc.3 → 1.0.82-rc.31

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 (55) hide show
  1. package/dist/assistant-protocol.d.ts +140 -6
  2. package/dist/index.d.ts +6 -0
  3. package/dist/index.js +529 -110
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +500 -110
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/launch-defaults.d.ts +194 -0
  8. package/dist/mesh-tool-names.d.ts +2 -2
  9. package/dist/mesh-vocabulary.d.ts +13 -4
  10. package/dist/node-facts.d.ts +41 -1
  11. package/dist/outbound-message.d.ts +7 -0
  12. package/dist/path-ownership.d.ts +1 -1
  13. package/dist/protocol/daemon-server.d.ts +9 -0
  14. package/dist/protocol/dashboard-daemon-p2p.d.ts +1 -1
  15. package/dist/protocol/dashboard-server.d.ts +10 -0
  16. package/dist/provider-approval.d.ts +36 -0
  17. package/dist/provider-question.d.ts +19 -0
  18. package/dist/quota-freshness.d.ts +65 -0
  19. package/dist/role-launch.d.ts +11 -0
  20. package/dist/session-launch.d.ts +3 -22
  21. package/dist/slot-proposal.d.ts +14 -0
  22. package/dist/task-source-phase.d.ts +75 -0
  23. package/dist/turn-evidence.d.ts +24 -12
  24. package/dist/turn-ipc-mission.d.ts +4 -0
  25. package/dist/turn-ipc-queue.d.ts +19 -0
  26. package/dist/turn-ipc.d.ts +2 -2
  27. package/dist/types.d.ts +1 -0
  28. package/dist/worker-protocol.d.ts +1 -1
  29. package/package.json +1 -1
  30. package/src/assistant-protocol.ts +247 -6
  31. package/src/daemon-normalize.ts +7 -5
  32. package/src/index.ts +7 -0
  33. package/src/keyed-doc-delta.ts +9 -1
  34. package/src/launch-defaults.ts +168 -0
  35. package/src/mesh-tool-names.ts +5 -0
  36. package/src/mesh-vocabulary.ts +12 -2
  37. package/src/node-facts.ts +36 -1
  38. package/src/outbound-message.ts +17 -0
  39. package/src/path-ownership.ts +4 -6
  40. package/src/protocol/daemon-server.ts +7 -2
  41. package/src/protocol/dashboard-server.ts +8 -2
  42. package/src/provider-approval.ts +59 -0
  43. package/src/provider-question.ts +31 -0
  44. package/src/quota-freshness.ts +139 -0
  45. package/src/role-launch.ts +51 -0
  46. package/src/session-launch.ts +8 -1
  47. package/src/slot-proposal.ts +61 -2
  48. package/src/task-source-phase.ts +61 -0
  49. package/src/turn-evidence.ts +22 -19
  50. package/src/turn-ipc-mission.ts +8 -2
  51. package/src/turn-ipc-queue.ts +29 -1
  52. package/src/turn-ipc-turn.ts +1 -1
  53. package/src/turn-ipc.ts +1 -0
  54. package/src/types.ts +1 -0
  55. package/src/worker-protocol.ts +4 -0
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'
@@ -41,3 +42,9 @@ export * from './turn-ipc'
41
42
  export * from './path-ownership'
42
43
  export * from './mission-brief'
43
44
  export * from './keyed-doc-delta'
45
+ export * from './provider-approval'
46
+ export * from './provider-question'
47
+ export * from './launch-defaults'
48
+
49
+ export * from './role-launch';
50
+ export * from './task-source-phase';
@@ -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,168 @@
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
+ }
28
+ export interface LaunchDefaultChange {
29
+ at: number;
30
+ role: LaunchDefaultRole;
31
+ by: 'owner' | 'assistant';
32
+ origin: LaunchDefaultOrigin;
33
+ from: LaunchDefaultEntry | null;
34
+ to: LaunchDefaultEntry | null;
35
+ }
36
+ export interface LaunchDefaultsDocument {
37
+ version: 1;
38
+ /** Global document token, including clears; distinct from entry revisions. */
39
+ revision: number;
40
+ assistant?: LaunchDefaultEntry;
41
+ coordinator?: LaunchDefaultEntry;
42
+ history: LaunchDefaultChange[];
43
+ }
44
+ export interface LaunchDefaultRef {
45
+ role: LaunchDefaultRole;
46
+ scope: 'global' | 'project';
47
+ /** Entry revision for global defaults; host project token for project defaults. */
48
+ revision: number;
49
+ projectId?: string;
50
+ }
51
+ export type LaunchDefaultField = keyof LaunchDefaultSelection;
52
+ export interface LaunchDefaultAxis {
53
+ value: string | null;
54
+ source: LaunchDefaultSource;
55
+ defaultRef?: LaunchDefaultRef;
56
+ }
57
+ export type LaunchDefaultWarning = 'catalog_unverified' | 'manual_mcp_setup_required';
58
+ export type LaunchDefaultValidationReason =
59
+ | 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled'
60
+ | 'provider_not_detected' | 'provider_not_eligible' | 'provider_quota_gated' | 'model_unsupported'
61
+ | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported'
62
+ | 'thinking_not_offered' | 'catalog_unavailable';
63
+ export type LaunchDefaultResolution =
64
+ | { ok: true; selection: LaunchDefaultSelection; applied: Record<LaunchDefaultField, LaunchDefaultAxis>;
65
+ defaultRef?: LaunchDefaultRef; documentRevision: number; warnings: LaunchDefaultWarning[];
66
+ toolRestriction: LaunchDefaultToolRestriction }
67
+ | { ok: false; code: 'launch_default_required' | 'launch_default_unavailable' | 'launch_selection_invalid';
68
+ role: LaunchDefaultRole; field: LaunchDefaultField; reason: string; source?: LaunchDefaultSource;
69
+ value?: string | null; defaultRef?: LaunchDefaultRef; candidates?: readonly string[]; hint: string };
70
+ export interface LaunchDefaultsRoleView {
71
+ stored: LaunchDefaultEntry | null;
72
+ /** The existing daemon resolver's result, including loud failures. */
73
+ effective: LaunchDefaultResolution;
74
+ }
75
+ type Target = { role: 'assistant'; project?: never } | { role: 'coordinator'; project?: string };
76
+ export type LaunchDefaultsReadRequest =
77
+ | ({ action?: 'get' } & (Target | { role?: never; project?: never }))
78
+ | ({ action: 'check' } & Target & LaunchDefaultSelection);
79
+ /** Mandatory CAS: global document token, or host project token for project writes.
80
+ * 0 is explicit first setup; clears advance the token and never restore fallback. */
81
+ export type LaunchDefaultsSetRequest = Target & { expectedRevision: number } & (
82
+ | ({ action: 'set' } & LaunchDefaultSelection)
83
+ | { action: 'clear'; provider?: never; model?: never; thinkingLevel?: never }
84
+ );
85
+ /** Existing assistant routing envelope; session identity never supplies origin. */
86
+ export type AssistantLaunchDefaultsReadRequest = LaunchDefaultsReadRequest & { assistantSessionId?: string };
87
+ export type AssistantLaunchDefaultsSetRequest = LaunchDefaultsSetRequest & { assistantSessionId?: string };
88
+ export const LAUNCH_DEFAULT_ERROR_CODES = [
89
+ 'invalid_args', 'launch_default_required', 'launch_default_unavailable', 'launch_selection_invalid',
90
+ 'launch_default_invalid', 'revision_conflict', 'store_unavailable', 'store_busy', 'persistence_failed',
91
+ 'project_not_found', 'project_ambiguous', 'project_not_hosted', 'project_storage_unavailable',
92
+ 'staging_unavailable', 'staging_failed', 'review_turn_tool_denied',
93
+ ] as const;
94
+ export type LaunchDefaultsErrorCode = typeof LAUNCH_DEFAULT_ERROR_CODES[number];
95
+ export interface LaunchDefaultsError {
96
+ success: false;
97
+ code: LaunchDefaultsErrorCode;
98
+ error: string;
99
+ reason?: string;
100
+ role?: LaunchDefaultRole;
101
+ field?: LaunchDefaultField;
102
+ source?: LaunchDefaultSource;
103
+ value?: string | null;
104
+ defaultRef?: LaunchDefaultRef;
105
+ candidates?: readonly string[];
106
+ actualRevision?: number;
107
+ /** 0 for an absent global document; absent when it cannot be read. */
108
+ documentRevision?: number;
109
+ /** A persistence failure can replace intent without confirming durability.
110
+ * Re-read before retrying; never report applied on committed:true alone. */
111
+ committed?: boolean;
112
+ }
113
+ export interface LaunchDefaultsReadSuccess {
114
+ success: true;
115
+ result: 'get';
116
+ documentRevision: number;
117
+ roles: Partial<Record<LaunchDefaultRole, LaunchDefaultsRoleView>>;
118
+ project?: { projectId: string; revision: number; entry: LaunchDefaultEntry | null };
119
+ }
120
+ export interface LaunchDefaultsCheckSuccess {
121
+ success: true;
122
+ result: 'check';
123
+ role: LaunchDefaultRole;
124
+ selection: LaunchDefaultSelection;
125
+ warnings: LaunchDefaultWarning[];
126
+ toolRestriction: LaunchDefaultToolRestriction;
127
+ lowersToolLock: boolean;
128
+ }
129
+ export interface LaunchDefaultsApplied {
130
+ success: true;
131
+ result: 'applied';
132
+ role: LaunchDefaultRole;
133
+ scope: 'global' | 'project';
134
+ projectId?: string;
135
+ entry: LaunchDefaultEntry | null;
136
+ previous: LaunchDefaultEntry | null;
137
+ /** CAS token returned by the authority that performed the write. */
138
+ revision: number;
139
+ warnings: LaunchDefaultWarning[];
140
+ durable: true;
141
+ note: string;
142
+ }
143
+ export interface LaunchDefaultsStaged {
144
+ success: true;
145
+ result: 'staged';
146
+ role: LaunchDefaultRole;
147
+ stagedId: string;
148
+ previous: LaunchDefaultEntry | null;
149
+ selection: LaunchDefaultSelection | null;
150
+ expectedRevision: number;
151
+ warnings: LaunchDefaultWarning[];
152
+ note: string;
153
+ }
154
+ /** Existing owner Inbox adapter stores this proposal; it is not another default.
155
+ * Applying it must revalidate on the host and CAS via the same set handler. */
156
+ export interface LaunchDefaultsProposal {
157
+ kind: 'launch_defaults';
158
+ role: LaunchDefaultRole;
159
+ project?: string;
160
+ selection: LaunchDefaultSelection | null;
161
+ previous: LaunchDefaultEntry | null;
162
+ expectedRevision: number;
163
+ origin: LaunchDefaultOrigin;
164
+ callerSessionId?: string;
165
+ }
166
+ export type LaunchDefaultsReadResponse = LaunchDefaultsReadSuccess | LaunchDefaultsCheckSuccess | LaunchDefaultsError;
167
+ export type LaunchDefaultsSetResponse = LaunchDefaultsApplied | LaunchDefaultsStaged | LaunchDefaultsError;
168
+ export type LaunchDefaultsResponse = LaunchDefaultsReadResponse | LaunchDefaultsSetResponse;
@@ -28,6 +28,7 @@ export const CANONICAL_MESH_TOOL_NAMES = [
28
28
  'mesh_view_queue',
29
29
  'mesh_queue_cancel',
30
30
  'mesh_queue_requeue',
31
+ 'mesh_queue_revise',
31
32
  'mesh_send_task',
32
33
  'mesh_read_chat',
33
34
  'mesh_read_debug',
@@ -44,6 +45,7 @@ export const CANONICAL_MESH_TOOL_NAMES = [
44
45
  'mesh_list_pending_approvals',
45
46
  'mesh_create',
46
47
  'mesh_add_node',
48
+ 'mesh_attach_remote',
47
49
  'mesh_clone_node',
48
50
  'mesh_remove_node',
49
51
  'mesh_cleanup_worktree_nodes',
@@ -64,6 +66,9 @@ export const CANONICAL_MESH_TOOL_NAMES = [
64
66
  'mesh_mission_list',
65
67
  'mesh_review_inbox',
66
68
  'mesh_node_slots',
69
+ 'driver_list',
70
+ 'driver_describe',
71
+ 'artifact_list',
67
72
  ] as const;
68
73
 
69
74
  export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
@@ -17,10 +17,12 @@
17
17
 
18
18
  export { MESH_TASK_DIFFICULTIES, type MeshTaskDifficulty } from './brain-routing'
19
19
 
20
- export const MESH_TASK_STATUSES = ['pending', 'assigned', 'completed', 'failed', 'cancelled'] as const
20
+ /** `blocked` ends a worker attempt pending external follow-up; only completed satisfies dependencies.
21
+ * Branch convergence (including blocked_review) remains an independent axis. */
22
+ export const MESH_TASK_STATUSES = ['pending', 'assigned', 'completed', 'blocked', 'failed', 'cancelled'] as const
21
23
  export type MeshTaskStatus = typeof MESH_TASK_STATUSES[number]
22
24
 
23
- export const MESH_TERMINAL_TASK_STATUSES = ['completed', 'failed', 'cancelled'] as const
25
+ export const MESH_TERMINAL_TASK_STATUSES = ['completed', 'blocked', 'failed', 'cancelled'] as const
24
26
  export type MeshTerminalTaskStatus = typeof MESH_TERMINAL_TASK_STATUSES[number]
25
27
 
26
28
  export const MESH_TASK_MODES = ['code_change', 'validation', 'live_debug_readonly', 'launch_app', 'convergence'] as const
@@ -56,6 +58,14 @@ export type MeshSessionCleanupMode = typeof MESH_SESSION_CLEANUP_MODES[number]
56
58
  export const WORKER_REPORT_OUTCOMES = ['completed', 'blocked', 'failed'] as const
57
59
  export type WorkerReportOutcome = typeof WORKER_REPORT_OUTCOMES[number]
58
60
 
61
+ /**
62
+ * Hard cap on a `report_completion` summary (characters, after trim). Over-limit
63
+ * summaries are REJECTED, never truncated. Declared once so the daemon validator
64
+ * and the mcp-server's pre-send check (which keeps an over-limit report out of the
65
+ * durable outbox) cannot drift apart.
66
+ */
67
+ export const WORKER_REPORT_SUMMARY_MAX_CHARS = 8_000
68
+
59
69
  /** Final branch state a worker reports (see coordinator convergence rules). */
60
70
  export const WORKER_BRANCH_STATES = [
61
71
  'merged_to_main',
package/src/node-facts.ts CHANGED
@@ -97,6 +97,20 @@ export interface MeshNodeFactsProviderQuota {
97
97
  * rather than presenting them as a freshly measured value.
98
98
  */
99
99
  lastGoodWindows?: boolean
100
+ /**
101
+ * Unix ms the reporting node's NEXT scheduled retry of this transient
102
+ * failure is due (HTTP Retry-After wins when later). Re-stamped to the
103
+ * real backoff time on every failure — see daemon-core
104
+ * `updateFailureRetry`. Absent on persistent failures and on daemons
105
+ * predating the stamp.
106
+ */
107
+ retryAtMs?: number
108
+ /**
109
+ * True once the node has spent its bounded transient-retry budget and
110
+ * stopped retrying; the next look is the hourly backfill. Readers must
111
+ * stop calling retained numbers "refreshing" (`assessQuotaFreshness`).
112
+ */
113
+ retryExhausted?: boolean
100
114
  /**
101
115
  * Unix ms when the reporting node last ATTEMPTED a refresh of this
102
116
  * provider — deliberately distinct from `updatedAt`, which dates the
@@ -112,6 +126,25 @@ export interface MeshNodeFactsProviderQuota {
112
126
  * predating the field.
113
127
  */
114
128
  fetchedAt?: number
129
+ /**
130
+ * Provenance of the RETAINED reading when `lastGoodWindows` is true —
131
+ * the source/plan/measurement time of the snapshot that produced the
132
+ * carried numbers, as opposed to the top-level `source`/`planType`,
133
+ * which describe the fresh failed attempt. See daemon-core
134
+ * `QuotaMetadata.lastGood`.
135
+ */
136
+ lastGood?: {
137
+ measuredAt: number
138
+ source?: string
139
+ planType?: string | null
140
+ collectorScope?: string
141
+ }
142
+ /**
143
+ * `'background-collector'` when the snapshot came from a daemon-spawned
144
+ * probe rather than an executing CLI session; a failure with this scope
145
+ * is not evidence about a running session's sign-in.
146
+ */
147
+ collectorScope?: string
115
148
  [extra: string]: unknown
116
149
  }
117
150
  [extra: string]: unknown
@@ -137,6 +170,8 @@ export interface MeshNodeFactsProviderEnablement {
137
170
 
138
171
  export interface MeshNodeFacts {
139
172
  schemaVersion: number
173
+ /** Cached CLI detection only; no paths or account data. */
174
+ providerInventory?: { observedAt: number; providers: Record<string, { installed: boolean }> }
140
175
  reportedAt: number
141
176
  daemonBuild?: MeshNodeFactsDaemonBuild
142
177
  providerVersions?: Record<string, string>
@@ -253,7 +288,7 @@ export function normalizeMeshNodeFacts(raw: unknown): MeshNodeFacts | undefined
253
288
  * the host `agy` itself uses; the unprefixed `cloudcode-pa` host 429s Google
254
289
  * AI Pro Antigravity accounts), and its credential lives in the OS keyring,
255
290
  * not the stale on-disk token file — see the provenance note in daemon-core
256
- * `quota/fetchers/antigravity.ts`. Supported on macOS and Windows; other
291
+ * `quota/fetchers/antigravity.ts`. Supported on macOS, Windows, and Linux; other
257
292
  * platforms report `unsupported` rather than guess at a keyring backend
258
293
  * nobody has verified.
259
294
  *
@@ -321,3 +321,20 @@ export function isOutboundMessage(value: unknown): value is OutboundMessage {
321
321
  && isSendPolicy(candidate.policy)
322
322
  && typeof candidate.createdAt === 'number'
323
323
  }
324
+
325
+ const PASTED_OPEN_RE = /^\s*<pasted_content(?:\s+id="[^"]*")?\s*>\s*$/
326
+ const PASTED_CLOSE_RE = /^\s*<\/pasted_content(?:\s+id="[^"]*")?\s*>\s*$/
327
+
328
+ /**
329
+ * Drop the `<pasted_content>` envelope lines Claude Code wraps around a pasted
330
+ * input. Only a text that OPENS with the envelope is touched; anything else is
331
+ * returned unchanged. Shared across daemon transcript merge and web echo/display
332
+ * normalization so wrapped turns correlate identically.
333
+ */
334
+ export function unwrapPastedContent(text: string): string {
335
+ const lines = text.replace(/\r\n?/g, '\n').split('\n')
336
+ const first = lines.findIndex((line) => line.trim())
337
+ if (first < 0 || !PASTED_OPEN_RE.test(lines[first])) return text
338
+ return lines.filter((line) => !PASTED_OPEN_RE.test(line) && !PASTED_CLOSE_RE.test(line)).join('\n')
339
+ }
340
+
@@ -34,7 +34,7 @@
34
34
 
35
35
  /** One normalized ownership entry: a repo-relative path, optionally a `**` subtree prefix. */
36
36
  export interface OwnedPathEntry {
37
- /** Repo-relative path with forward slashes, no leading `./`, no trailing slash. */
37
+ /** Repo-relative path with forward slashes, no leading `./`, no trailing slash; empty for the whole-repo subtree. */
38
38
  readonly path: string
39
39
  /** True when this entry was declared as `<path>/**` (owns the whole subtree). */
40
40
  readonly subtree: boolean
@@ -159,16 +159,14 @@ export function normalizeOwnedPaths(input: unknown): NormalizeOwnedPathsResult {
159
159
  /** True when `path` is exactly `base`, or (when `base` is a subtree entry) nested under it. */
160
160
  function entryContains(base: OwnedPathEntry, path: string): boolean {
161
161
  if (path === base.path) return true
162
- if (base.subtree && (path === base.path || path.startsWith(`${base.path}/`))) return true
162
+ // An empty subtree prefix is the repo root, so it contains every normalized path.
163
+ if (base.subtree && (base.path === '' || path.startsWith(`${base.path}/`))) return true
163
164
  return false
164
165
  }
165
166
 
166
167
  /** True when two normalized entries' claimed sets intersect (exact match, or one contains the other's directory). */
167
168
  function entriesOverlap(a: OwnedPathEntry, b: OwnedPathEntry): boolean {
168
- if (a.path === b.path) return true
169
- if (a.subtree && (b.path === a.path || b.path.startsWith(`${a.path}/`))) return true
170
- if (b.subtree && (a.path === b.path || a.path.startsWith(`${b.path}/`))) return true
171
- return false
169
+ return entryContains(a, b.path) || entryContains(b, a.path)
172
170
  }
173
171
 
174
172
  /** True when two declarations claim any overlapping path. Pure set comparison — no policy (refuse vs warn) here. */
@@ -73,6 +73,9 @@ export type DaemonStatusEventWirePayload = {
73
73
  elapsedSec?: number
74
74
  modalMessage?: string
75
75
  modalButtons?: string[]
76
+ modalKey?: string
77
+ approvalEvidence?: 'live' | 'cached' | 'unavailable'
78
+ waitKind?: 'external'
76
79
  surfaceHidden?: boolean
77
80
  muted?: boolean
78
81
  }
@@ -260,8 +263,8 @@ export const DAEMON_P2P_SIGNAL_TYPES = ['p2p_offer', 'p2p_ice'] as const
260
263
  export type DaemonP2PSignalType = typeof DAEMON_P2P_SIGNAL_TYPES[number]
261
264
  export const isDaemonP2PSignalType = makeTypeGuard(DAEMON_P2P_SIGNAL_TYPES)
262
265
 
263
- export type P2POfferWirePayload = { sdp: string; type: string; peerId: string }
264
- export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string }
266
+ export type P2POfferWirePayload = { sdp: string; type: string; peerId: string; attemptId?: number; attemptEpoch?: string }
267
+ export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string; attemptId?: number; attemptEpoch?: string }
265
268
 
266
269
  /**
267
270
  * A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
@@ -273,6 +276,8 @@ export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string
273
276
  export type RelayedP2PSignalWirePayload = {
274
277
  peerId?: string
275
278
  daemonId?: string
279
+ attemptId?: number
280
+ attemptEpoch?: string
276
281
  sharePermission?: string
277
282
  shareTargetSessionId?: string
278
283
  sdp?: string
@@ -152,6 +152,8 @@ export type SystemAnnouncementWirePayload = {
152
152
  export type DashboardP2POfferWirePayload = {
153
153
  daemonId: string
154
154
  peerId?: string
155
+ attemptId?: number
156
+ attemptEpoch?: string
155
157
  sdp: string
156
158
  type: string
157
159
  }
@@ -159,6 +161,8 @@ export type DashboardP2POfferWirePayload = {
159
161
  export type DashboardP2PIceWirePayload = {
160
162
  daemonId: string
161
163
  peerId?: string
164
+ attemptId?: number
165
+ attemptEpoch?: string
162
166
  candidate: string
163
167
  mid?: string
164
168
  sdpMid?: string | null
@@ -234,11 +238,13 @@ export const DASHBOARD_P2P_SIGNAL_TYPES = ['p2p_ready', 'p2p_answer', 'p2p_ice']
234
238
  export type DashboardP2PSignalType = typeof DASHBOARD_P2P_SIGNAL_TYPES[number]
235
239
  export const isDashboardP2PSignalType = makeTypeGuard(DASHBOARD_P2P_SIGNAL_TYPES)
236
240
 
237
- export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string }
238
- export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string }
241
+ export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string; attemptId?: number; attemptEpoch?: string }
242
+ export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string; attemptId?: number; attemptEpoch?: string }
239
243
  export type DashboardP2PLocalIceWirePayload = {
240
244
  daemonId: string
241
245
  peerId: string
246
+ attemptId?: number
247
+ attemptEpoch?: string
242
248
  candidate: string
243
249
  sdpMid: string | null
244
250
  sdpMLineIndex: number | null
@@ -0,0 +1,59 @@
1
+ /** Provider permissions are runtime decisions, never owner write ratification. */
2
+ export interface ProviderApproval {
3
+ source: 'provider_modal' | 'legacy_provider' | 'status_only' | 'ledger_approval';
4
+ /** Exact Inbox actions or the existing session handler; never inferred consent. */
5
+ compatibility?: 'exact' | 'session_only' | 'unknown';
6
+ type: 'tool_permission' | 'provider_setup' | 'unknown';
7
+ evidence: 'live' | 'cached' | 'unavailable';
8
+ lifecycle: 'pending' | 'resolved';
9
+ modalKey: string | null;
10
+ createdAt: number | null;
11
+ resolvedAt: number | null;
12
+ description: string;
13
+ runtimeDirectory?: string;
14
+ displayedDirectory?: string | null;
15
+ buttons: string[];
16
+ actionable: boolean;
17
+ providerType?: string;
18
+ sessionId?: string;
19
+ nodeId?: string | null;
20
+ meshId?: string | null;
21
+ taskId?: string | null;
22
+ attemptId?: string | null;
23
+ generation?: number | null;
24
+ }
25
+
26
+ /** Exact resource identity required by the approvals-only action path. */
27
+ export interface ProviderApprovalTarget {
28
+ modalKey: string;
29
+ attemptId: string | null;
30
+ generation: number | null;
31
+ }
32
+
33
+ /** Confirmed authority for the new Inbox path. Legacy handlers retain their own checks. */
34
+ export function isActionableProviderApproval(approval: ProviderApproval | null | undefined): boolean {
35
+ return !!approval && approval.source === 'provider_modal' && approval.type === 'tool_permission'
36
+ && approval.lifecycle === 'pending' && approval.evidence === 'live' && approval.actionable
37
+ && !!approval.modalKey && Array.isArray(approval.buttons)
38
+ && approval.buttons.some(button => typeof button === 'string' && !!button.trim());
39
+ }
40
+
41
+ /** An explicit null suppresses the resource; only missing metadata uses compatibility. */
42
+ export function readProviderDecision(approval: ProviderApproval | null | undefined, waiting: boolean, hasModal = false): ProviderApproval | null {
43
+ if (approval !== undefined) return approval && ['provider_modal', 'legacy_provider', 'status_only', 'ledger_approval'].includes(approval.source) ? approval : null;
44
+ if (!waiting) return null;
45
+ return { source: hasModal ? 'legacy_provider' : 'status_only', type: 'unknown',
46
+ compatibility: hasModal ? 'session_only' : 'unknown', evidence: 'unavailable',
47
+ lifecycle: 'pending', modalKey: null, createdAt: null, resolvedAt: null,
48
+ description: '', buttons: [], actionable: false };
49
+ }
50
+
51
+ /** Host metadata is durable: identity and clocks only, never modal text or directories. */
52
+ export function providerApprovalSnapshot(approval: ProviderApproval | null | undefined): ProviderApproval | null {
53
+ if (!approval) return null;
54
+ const { source, type, lifecycle, modalKey, createdAt, resolvedAt, providerType, sessionId,
55
+ nodeId, meshId, taskId, attemptId, generation } = approval;
56
+ return { source, type, lifecycle, modalKey, createdAt, resolvedAt, providerType, sessionId,
57
+ nodeId, meshId, taskId, attemptId, generation, evidence: 'cached', actionable: false,
58
+ description: '', buttons: [] };
59
+ }
@@ -0,0 +1,31 @@
1
+ /** Ephemeral provenance supplied by the owning daemon, never inferred from prompt text. */
2
+ export interface ProviderQuestionBinding {
3
+ transport: 'codex_rpc' | 'codex_async';
4
+ sessionId: string;
5
+ generation: string;
6
+ threadId: string;
7
+ turnId: string;
8
+ itemId: string;
9
+ requestId?: string | number;
10
+ }
11
+
12
+ export type ProviderQuestionLifecycle = 'pending' | 'submitted' | 'resolved' | 'completed' | 'cancelled' | 'expired' | 'unknown';
13
+
14
+ export interface ProviderQuestionEpisode {
15
+ blocking?: boolean;
16
+ binding?: ProviderQuestionBinding;
17
+ lifecycle?: ProviderQuestionLifecycle;
18
+ /** Async dismiss is product-local; it is not a native tool cancellation. */
19
+ support?: 'request_bound' | 'product_local' | 'uncertain';
20
+ }
21
+
22
+ export function isProviderQuestionBinding(value: unknown): value is ProviderQuestionBinding {
23
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
24
+ const b = value as Record<string, unknown>;
25
+ if (b.transport !== 'codex_rpc' && b.transport !== 'codex_async') return false;
26
+ const keys = ['transport', 'sessionId', 'generation', 'threadId', 'turnId', 'itemId', ...(b.transport === 'codex_rpc' ? ['requestId'] : [])];
27
+ if (Reflect.ownKeys(b).some(key => typeof key !== 'string' || !keys.includes(key))) return false;
28
+ if (['sessionId', 'generation', 'threadId', 'turnId', 'itemId'].some(key => typeof b[key] !== 'string' || !(b[key] as string).trim())) return false;
29
+ return b.transport === 'codex_async' ? b.requestId === undefined
30
+ : (typeof b.requestId === 'string' && !!b.requestId.trim()) || (typeof b.requestId === 'number' && Number.isFinite(b.requestId));
31
+ }