@adhdev/mesh-shared 1.0.82-rc.10 → 1.0.82-rc.12

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.
@@ -49,7 +49,7 @@ export type WorkerReportOutcome = typeof WORKER_REPORT_OUTCOMES[number];
49
49
  /** Final branch state a worker reports (see coordinator convergence rules). */
50
50
  export declare const WORKER_BRANCH_STATES: readonly ["merged_to_main", "pushed_feature_branch_needs_merge", "blocked_review", "cleanup_candidate", "not_mergeable"];
51
51
  export type WorkerBranchState = typeof WORKER_BRANCH_STATES[number];
52
- export declare const isMeshTaskStatus: (value: unknown) => value is "failed" | "pending" | "assigned" | "completed" | "cancelled";
52
+ export declare const isMeshTaskStatus: (value: unknown) => value is "pending" | "failed" | "assigned" | "completed" | "cancelled";
53
53
  export declare const isMeshTerminalTaskStatus: (value: unknown) => value is "failed" | "completed" | "cancelled";
54
54
  export declare const isMeshTaskMode: (value: unknown) => value is "code_change" | "validation" | "live_debug_readonly" | "launch_app" | "convergence";
55
55
  export declare const isMeshTaskPriority: (value: unknown) => value is "low" | "high" | "normal";
@@ -146,6 +146,13 @@ export interface MeshNodeFactsProviderEnablement {
146
146
  }
147
147
  export interface MeshNodeFacts {
148
148
  schemaVersion: number;
149
+ /** Cached CLI detection only; no paths or account data. */
150
+ providerInventory?: {
151
+ observedAt: number;
152
+ providers: Record<string, {
153
+ installed: boolean;
154
+ }>;
155
+ };
149
156
  reportedAt: number;
150
157
  daemonBuild?: MeshNodeFactsDaemonBuild;
151
158
  providerVersions?: Record<string, string>;
@@ -33,7 +33,7 @@
33
33
  */
34
34
  /** One normalized ownership entry: a repo-relative path, optionally a `**` subtree prefix. */
35
35
  export interface OwnedPathEntry {
36
- /** Repo-relative path with forward slashes, no leading `./`, no trailing slash. */
36
+ /** Repo-relative path with forward slashes, no leading `./`, no trailing slash; empty for the whole-repo subtree. */
37
37
  readonly path: string;
38
38
  /** True when this entry was declared as `<path>/**` (owns the whole subtree). */
39
39
  readonly subtree: boolean;
@@ -0,0 +1,36 @@
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
+ /** Exact resource identity required by the approvals-only action path. */
26
+ export interface ProviderApprovalTarget {
27
+ modalKey: string;
28
+ attemptId: string | null;
29
+ generation: number | null;
30
+ }
31
+ /** Confirmed authority for the new Inbox path. Legacy handlers retain their own checks. */
32
+ export declare function isActionableProviderApproval(approval: ProviderApproval | null | undefined): boolean;
33
+ /** An explicit null suppresses the resource; only missing metadata uses compatibility. */
34
+ export declare function readProviderDecision(approval: ProviderApproval | null | undefined, waiting: boolean, hasModal?: boolean): ProviderApproval | null;
35
+ /** Host metadata is durable: identity and clocks only, never modal text or directories. */
36
+ export declare function providerApprovalSnapshot(approval: ProviderApproval | null | undefined): ProviderApproval | null;
@@ -0,0 +1,19 @@
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
+ export type ProviderQuestionLifecycle = 'pending' | 'submitted' | 'resolved' | 'completed' | 'cancelled' | 'expired' | 'unknown';
12
+ export interface ProviderQuestionEpisode {
13
+ blocking?: boolean;
14
+ binding?: ProviderQuestionBinding;
15
+ lifecycle?: ProviderQuestionLifecycle;
16
+ /** Async dismiss is product-local; it is not a native tool cancellation. */
17
+ support?: 'request_bound' | 'product_local' | 'uncertain';
18
+ }
19
+ export declare function isProviderQuestionBinding(value: unknown): value is ProviderQuestionBinding;
@@ -58,20 +58,20 @@ export declare const SESSION_REBOUND_REASONS: readonly ["restart", "live_holder"
58
58
  export type SessionReboundReason = typeof SESSION_REBOUND_REASONS[number];
59
59
  export declare const SUSPENSION_MODALS: readonly ["approval", "choice"];
60
60
  export type SuspensionModal = typeof SUSPENSION_MODALS[number];
61
- export declare const SUSPENSION_RESOLUTIONS: readonly ["approved", "rejected", "answered"];
61
+ export declare const SUSPENSION_RESOLUTIONS: readonly ["approved", "rejected", "answered", "dismissed"];
62
62
  export type SuspensionResolution = typeof SUSPENSION_RESOLUTIONS[number];
63
- export declare const SUSPENSION_RESOLUTION_VIAS: readonly ["modal_button", "auto_approve", "prompt_answer"];
63
+ export declare const SUSPENSION_RESOLUTION_VIAS: readonly ["modal_button", "auto_approve", "prompt_answer", "modal_disappeared"];
64
64
  export type SuspensionResolutionVia = typeof SUSPENSION_RESOLUTION_VIAS[number];
65
65
  export declare const TURN_END_STRENGTHS: readonly ["genuine", "weak"];
66
66
  export type TurnEndStrength = typeof TURN_END_STRENGTHS[number];
67
67
  /** Why a provider-side weak end was released (was free text `blockReason`). */
68
68
  export declare const TURN_END_BLOCK_REASONS: readonly ["missing_final_assistant", "finalization_timeout", "terminal_block_hard_cap", "decoupled_completion"];
69
69
  export type TurnEndBlockReason = typeof TURN_END_BLOCK_REASONS[number];
70
- export declare const NATIVE_TURN_OUTCOMES: readonly ["completed", "aborted"];
70
+ export declare const NATIVE_TURN_OUTCOMES: readonly ["completed", "aborted", "failed"];
71
71
  export type NativeTurnOutcome = typeof NATIVE_TURN_OUTCOMES[number];
72
72
  export declare const LIVENESS_RESULTS: readonly ["alive", "unknown", "read_failed", "dead"];
73
73
  export type LivenessResult = typeof LIVENESS_RESULTS[number];
74
- export declare const PROVIDER_FAILURES: readonly ["auth_failed", "billing_failed"];
74
+ export declare const PROVIDER_FAILURES: readonly ["auth_failed", "billing_failed", "usage_limit_exceeded", "server_overloaded", "provider_error"];
75
75
  export type ProviderFailure = typeof PROVIDER_FAILURES[number];
76
76
  /**
77
77
  * `daemon_restart` (wiring-unification follow-up, design §5): boot-time
@@ -106,7 +106,7 @@ export type TurnOutcome = typeof TURN_OUTCOMES[number];
106
106
  export declare const COMMIT_STRENGTHS: readonly ["genuine", "weak", "tool_report", "operator"];
107
107
  export type CommitStrength = typeof COMMIT_STRENGTHS[number];
108
108
  /** Every terminal or reclaim reason the ledger writes. Closed: no free-text reasons. */
109
- export declare const TURN_REASONS: readonly ["turn_end", "weak_end_confirmed", "transcript_final", "worker_reported", "finalization_timeout_no_response", "session_error", "provider_auth_failed", "provider_billing_failed", "operator_cancel", "intentional_cleanup", "task_removed", "mission_abandoned", "superseded", "operator_update", "refine_terminal", "validation_terminal", "hard_ceiling", "reclaim_budget_exhausted", "hollow_max_retries", "migration_orphan", "dispatch_refused_session_exited", "dispatch_refused_no_target", "dispatch_refused_unsupported_input", "dispatch_failed", "session_exit", "session_exit_before_turn", "session_dead", "hollow_completion", "assigned_stranded_dispatch_unconfirmed", "delivered_not_consumed_redrive", "delivered_no_turn_deadline"];
109
+ export declare const TURN_REASONS: readonly ["turn_end", "weak_end_confirmed", "transcript_final", "worker_reported", "finalization_timeout_no_response", "session_error", "provider_auth_failed", "provider_billing_failed", "provider_usage_limit_exceeded", "provider_server_overloaded", "provider_error", "operator_cancel", "intentional_cleanup", "task_removed", "mission_abandoned", "superseded", "operator_update", "refine_terminal", "validation_terminal", "hard_ceiling", "reclaim_budget_exhausted", "hollow_max_retries", "migration_orphan", "dispatch_refused_session_exited", "dispatch_refused_no_target", "dispatch_refused_unsupported_input", "dispatch_failed", "session_exit", "session_exit_before_turn", "session_dead", "hollow_completion", "assigned_stranded_dispatch_unconfirmed", "delivered_not_consumed_redrive", "delivered_no_turn_deadline"];
110
110
  export type TurnReason = typeof TURN_REASONS[number];
111
111
  /**
112
112
  * What a `turn.notify` entry tells a coordinator.
@@ -149,6 +149,9 @@ export interface EvidenceCoordinatorIdentity {
149
149
  export interface NativeTurnMarkerRef {
150
150
  outcome: NativeTurnOutcome;
151
151
  turnId?: string;
152
+ receivedAt?: number;
153
+ startedAt?: number;
154
+ errorAt?: number;
152
155
  }
153
156
  export interface EvidenceEnvelope {
154
157
  /** Stable id of this observation; dedupe key across republish/replication. */
@@ -220,10 +223,13 @@ export type TurnEvidenceBody = {
220
223
  kind: 'suspension';
221
224
  modal: SuspensionModal;
222
225
  modalKey?: string;
226
+ providerSetup?: boolean;
227
+ clockUnknown?: boolean;
223
228
  } | {
224
229
  kind: 'suspension_resolved';
225
230
  resolution: SuspensionResolution;
226
231
  via: SuspensionResolutionVia;
232
+ modalKey?: string;
227
233
  } | {
228
234
  kind: 'turn_end';
229
235
  strength: TurnEndStrength;
@@ -233,6 +239,7 @@ export type TurnEvidenceBody = {
233
239
  blockReason?: TurnEndBlockReason;
234
240
  releasedByHardCap?: boolean;
235
241
  nativeOutcome?: NativeTurnOutcome;
242
+ nativeMarker?: NativeTurnMarkerRef;
236
243
  live?: LiveTurnPending;
237
244
  /**
238
245
  * Stamped (true only) by the WORKER's daemon when the session holds a live
@@ -372,13 +379,14 @@ export declare const TURN_EVIDENCE_KINDS: readonly TurnEvidenceKind[];
372
379
  /** Evidence kinds that, on their own, can end a turn (the ones R18/R19/R27 reason about). */
373
380
  export declare const TERMINAL_CLASS_EVIDENCE_KINDS: readonly ["turn_end", "transcript_final", "no_progress", "worker_report", "session_error", "process_exit", "cancel", "operator_status"];
374
381
  export declare const isTurnEvidenceKind: (value: unknown) => value is "no_progress" | "delivered" | "liveness" | "turn_end" | "transcript_final" | "session_error" | "dispatch_failed" | "dispatch_accepted" | "delivery_refused" | "duplicate_dispatch_refusal" | "session_rebound" | "turn_started" | "suspension" | "suspension_resolved" | "transcript_activity" | "process_exit" | "worker_report" | "worker_progress" | "git_side_effect" | "cancel" | "operator_status" | "coordinator_ack" | "hold_expired";
375
- export declare const isTurnReason: (value: unknown) => value is "intentional_cleanup" | "operator_cancel" | "task_removed" | "mission_abandoned" | "superseded" | "operator_update" | "refine_terminal" | "validation_terminal" | "hard_ceiling" | "turn_end" | "weak_end_confirmed" | "transcript_final" | "worker_reported" | "finalization_timeout_no_response" | "session_error" | "provider_auth_failed" | "provider_billing_failed" | "reclaim_budget_exhausted" | "hollow_max_retries" | "migration_orphan" | "dispatch_refused_session_exited" | "dispatch_refused_no_target" | "dispatch_refused_unsupported_input" | "dispatch_failed" | "session_exit" | "session_exit_before_turn" | "session_dead" | "hollow_completion" | "assigned_stranded_dispatch_unconfirmed" | "delivered_not_consumed_redrive" | "delivered_no_turn_deadline";
382
+ export declare const isTurnReason: (value: unknown) => value is "provider_error" | "intentional_cleanup" | "operator_cancel" | "task_removed" | "mission_abandoned" | "superseded" | "operator_update" | "refine_terminal" | "validation_terminal" | "hard_ceiling" | "turn_end" | "weak_end_confirmed" | "transcript_final" | "worker_reported" | "finalization_timeout_no_response" | "session_error" | "provider_auth_failed" | "provider_billing_failed" | "provider_usage_limit_exceeded" | "provider_server_overloaded" | "reclaim_budget_exhausted" | "hollow_max_retries" | "migration_orphan" | "dispatch_refused_session_exited" | "dispatch_refused_no_target" | "dispatch_refused_unsupported_input" | "dispatch_failed" | "session_exit" | "session_exit_before_turn" | "session_dead" | "hollow_completion" | "assigned_stranded_dispatch_unconfirmed" | "delivered_not_consumed_redrive" | "delivered_no_turn_deadline";
376
383
  export declare const isNotifyKind: (value: unknown) => value is "stopped" | "failed" | "no_progress" | "completed" | "cancelled" | "approval" | "choice" | "approval_resolved" | "candidate" | "progress" | "late_completion" | "mesh_event";
377
384
  export declare const isTurnOutcome: (value: unknown) => value is "failed" | "completed" | "cancelled";
378
385
  export declare const isCommitStrength: (value: unknown) => value is "operator" | "genuine" | "weak" | "tool_report";
379
386
  export declare function isEvidenceIdentifier(value: unknown): value is string;
380
387
  export declare function isSummaryRef(value: unknown): value is SummaryRef;
381
388
  export declare function isTurnAttemptRef(value: unknown): value is TurnAttemptRef;
389
+ export declare function isNativeMarkerRef(value: unknown): value is NativeTurnMarkerRef;
382
390
  /**
383
391
  * Strict structural guard: known kind, every declared field of the right class,
384
392
  * and NO undeclared field (an unknown key is how free text would sneak in).
package/dist/types.d.ts CHANGED
@@ -120,6 +120,7 @@ export interface MeshNodeIdentified {
120
120
  [key: string]: unknown;
121
121
  }
122
122
  export interface RepoMeshSessionStatus {
123
+ providerApproval?: import('./provider-approval').ProviderApproval | null;
123
124
  sessionId: string;
124
125
  providerType?: string;
125
126
  state?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/mesh-shared",
3
- "version": "1.0.82-rc.10",
3
+ "version": "1.0.82-rc.12",
4
4
  "description": "ADHDev mesh-shared — pure mesh/git status normalizers shared by daemon-core and web-core",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,6 +18,9 @@
18
18
 
19
19
  export const ASSISTANT_TOOLS = [
20
20
  'projects',
21
+ 'environment',
22
+ 'assistant_action',
23
+ 'project_watch',
21
24
  'project_status',
22
25
  'project_send',
23
26
  'project_read',
@@ -41,6 +44,11 @@ export function isAssistantTool(value: unknown): value is AssistantTool {
41
44
  export const ASSISTANT_VERB = {
42
45
  // tool verbs
43
46
  projects: 'assistant_projects',
47
+ environment: 'assistant_environment',
48
+ action: 'assistant_action',
49
+ watch: 'assistant_project_watch',
50
+ executionState: 'assistant_execution_state',
51
+ executionSettings: 'assistant_execution_settings',
44
52
  projectStatus: 'assistant_project_status',
45
53
  projectSend: 'assistant_project_send',
46
54
  projectRead: 'assistant_project_read',
@@ -64,6 +72,9 @@ export type AssistantVerb = typeof ASSISTANT_VERB[keyof typeof ASSISTANT_VERB]
64
72
  /** The daemon verb each assistant tool calls. */
65
73
  export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>> = {
66
74
  projects: ASSISTANT_VERB.projects,
75
+ environment: ASSISTANT_VERB.environment,
76
+ assistant_action: ASSISTANT_VERB.action,
77
+ project_watch: ASSISTANT_VERB.watch,
67
78
  project_status: ASSISTANT_VERB.projectStatus,
68
79
  project_send: ASSISTANT_VERB.projectSend,
69
80
  project_read: ASSISTANT_VERB.projectRead,
@@ -81,10 +92,11 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
81
92
  * because they cause effects (a coordinator message or launch, a new
82
93
  * project), and MCP clients use readOnlyHint to skip confirmation.
83
94
  */
84
- export const ASSISTANT_WRITE_TOOLS: readonly AssistantTool[] = ['memory', 'skill_manage', 'project_note', 'project_send', 'project_add']
95
+ export const ASSISTANT_WRITE_TOOLS: readonly AssistantTool[] = ['project_watch', 'assistant_action', 'memory', 'skill_manage', 'project_note', 'project_send', 'project_add']
85
96
 
86
97
  /** Owner-only verbs: never reachable from the assistant's MCP server (no `ipc`). */
87
98
  export const ASSISTANT_OWNER_VERBS: readonly AssistantVerb[] = [
99
+ ASSISTANT_VERB.executionSettings,
88
100
  ASSISTANT_VERB.stagedResolve,
89
101
  ASSISTANT_VERB.storeAdmin,
90
102
  ASSISTANT_VERB.importSkills,
package/src/index.ts CHANGED
@@ -42,3 +42,5 @@ export * from './turn-ipc'
42
42
  export * from './path-ownership'
43
43
  export * from './mission-brief'
44
44
  export * from './keyed-doc-delta'
45
+ export * from './provider-approval'
46
+ export * from './provider-question'
@@ -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
package/src/node-facts.ts CHANGED
@@ -151,6 +151,8 @@ export interface MeshNodeFactsProviderEnablement {
151
151
 
152
152
  export interface MeshNodeFacts {
153
153
  schemaVersion: number
154
+ /** Cached CLI detection only; no paths or account data. */
155
+ providerInventory?: { observedAt: number; providers: Record<string, { installed: boolean }> }
154
156
  reportedAt: number
155
157
  daemonBuild?: MeshNodeFactsDaemonBuild
156
158
  providerVersions?: Record<string, string>
@@ -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. */
@@ -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
+ }
@@ -83,9 +83,9 @@ export type SessionReboundReason = typeof SESSION_REBOUND_REASONS[number]
83
83
 
84
84
  export const SUSPENSION_MODALS = ['approval', 'choice'] as const
85
85
  export type SuspensionModal = typeof SUSPENSION_MODALS[number]
86
- export const SUSPENSION_RESOLUTIONS = ['approved', 'rejected', 'answered'] as const
86
+ export const SUSPENSION_RESOLUTIONS = ['approved', 'rejected', 'answered', 'dismissed'] as const
87
87
  export type SuspensionResolution = typeof SUSPENSION_RESOLUTIONS[number]
88
- export const SUSPENSION_RESOLUTION_VIAS = ['modal_button', 'auto_approve', 'prompt_answer'] as const
88
+ export const SUSPENSION_RESOLUTION_VIAS = ['modal_button', 'auto_approve', 'prompt_answer', 'modal_disappeared'] as const
89
89
  export type SuspensionResolutionVia = typeof SUSPENSION_RESOLUTION_VIAS[number]
90
90
 
91
91
  export const TURN_END_STRENGTHS = ['genuine', 'weak'] as const
@@ -97,13 +97,13 @@ export const TURN_END_BLOCK_REASONS = [
97
97
  ] as const
98
98
  export type TurnEndBlockReason = typeof TURN_END_BLOCK_REASONS[number]
99
99
 
100
- export const NATIVE_TURN_OUTCOMES = ['completed', 'aborted'] as const
100
+ export const NATIVE_TURN_OUTCOMES = ['completed', 'aborted', 'failed'] as const
101
101
  export type NativeTurnOutcome = typeof NATIVE_TURN_OUTCOMES[number]
102
102
 
103
103
  export const LIVENESS_RESULTS = ['alive', 'unknown', 'read_failed', 'dead'] as const
104
104
  export type LivenessResult = typeof LIVENESS_RESULTS[number]
105
105
 
106
- export const PROVIDER_FAILURES = ['auth_failed', 'billing_failed'] as const
106
+ export const PROVIDER_FAILURES = ['auth_failed', 'billing_failed', 'usage_limit_exceeded', 'server_overloaded', 'provider_error'] as const
107
107
  export type ProviderFailure = typeof PROVIDER_FAILURES[number]
108
108
 
109
109
  /**
@@ -164,7 +164,7 @@ export type CommitStrength = typeof COMMIT_STRENGTHS[number]
164
164
  export const TURN_REASONS = [
165
165
  // commits
166
166
  'turn_end', 'weak_end_confirmed', 'transcript_final', 'worker_reported',
167
- 'finalization_timeout_no_response', 'session_error', 'provider_auth_failed', 'provider_billing_failed',
167
+ 'finalization_timeout_no_response', 'session_error', 'provider_auth_failed', 'provider_billing_failed', 'provider_usage_limit_exceeded', 'provider_server_overloaded', 'provider_error',
168
168
  'operator_cancel', 'intentional_cleanup', 'task_removed', 'mission_abandoned', 'superseded',
169
169
  'operator_update', 'refine_terminal', 'validation_terminal',
170
170
  'hard_ceiling', 'reclaim_budget_exhausted', 'hollow_max_retries', 'migration_orphan',
@@ -207,7 +207,7 @@ export interface LiveTurnPending { modal: boolean; adapterPending: boolean; trai
207
207
  /** Structural copy of daemon-core's `CoordinatorIdentity` (this package cannot import daemon-core). */
208
208
  export interface EvidenceCoordinatorIdentity { daemonId: string; coordinatorRunId: string; sessionId?: string }
209
209
 
210
- export interface NativeTurnMarkerRef { outcome: NativeTurnOutcome; turnId?: string }
210
+ export interface NativeTurnMarkerRef { outcome: NativeTurnOutcome; turnId?: string; receivedAt?: number; startedAt?: number; errorAt?: number }
211
211
 
212
212
  export interface EvidenceEnvelope {
213
213
  /** Stable id of this observation; dedupe key across republish/replication. */
@@ -250,10 +250,10 @@ export type TurnEvidenceBody =
250
250
  | { kind: 'session_rebound'; toSessionId: string; reason: SessionReboundReason }
251
251
  // ── worker turn lifecycle ──
252
252
  | { kind: 'turn_started'; retro: boolean }
253
- | { kind: 'suspension'; modal: SuspensionModal; modalKey?: string }
254
- | { kind: 'suspension_resolved'; resolution: SuspensionResolution; via: SuspensionResolutionVia }
253
+ | { kind: 'suspension'; modal: SuspensionModal; modalKey?: string; providerSetup?: boolean; clockUnknown?: boolean }
254
+ | { kind: 'suspension_resolved'; resolution: SuspensionResolution; via: SuspensionResolutionVia; modalKey?: string }
255
255
  | { kind: 'turn_end'; strength: TurnEndStrength; afterFinalizationTimeout?: boolean; hollow?: boolean; summary?: SummaryRef
256
- blockReason?: TurnEndBlockReason; releasedByHardCap?: boolean; nativeOutcome?: NativeTurnOutcome; live?: LiveTurnPending
256
+ blockReason?: TurnEndBlockReason; releasedByHardCap?: boolean; nativeOutcome?: NativeTurnOutcome; nativeMarker?: NativeTurnMarkerRef; live?: LiveTurnPending
257
257
  /**
258
258
  * Stamped (true only) by the WORKER's daemon when the session holds a live
259
259
  * worker-MCP session bind, i.e. the worker can call report_completion. The
@@ -352,13 +352,13 @@ export const TURN_EVIDENCE_FIELD_SPECS: TurnEvidenceFieldSpecs = {
352
352
  duplicate_dispatch_refusal: { holderSessionId: id, holderAttemptId: idOpt },
353
353
  session_rebound: { toSessionId: id, reason: en(SESSION_REBOUND_REASONS) },
354
354
  turn_started: { retro: bool },
355
- suspension: { modal: en(SUSPENSION_MODALS), modalKey: idOpt },
356
- suspension_resolved: { resolution: en(SUSPENSION_RESOLUTIONS), via: en(SUSPENSION_RESOLUTION_VIAS) },
355
+ suspension: { modal: en(SUSPENSION_MODALS), modalKey: idOpt, providerSetup: boolOpt, clockUnknown: boolOpt },
356
+ suspension_resolved: { resolution: en(SUSPENSION_RESOLUTIONS), via: en(SUSPENSION_RESOLUTION_VIAS), modalKey: idOpt },
357
357
  turn_end: {
358
358
  strength: en(TURN_END_STRENGTHS), afterFinalizationTimeout: boolOpt, hollow: boolOpt,
359
359
  summary: { t: 'summary_ref', optional: true }, blockReason: enOpt(TURN_END_BLOCK_REASONS),
360
360
  releasedByHardCap: boolOpt, nativeOutcome: enOpt(NATIVE_TURN_OUTCOMES), live: { t: 'live', optional: true },
361
- reportExpected: boolOpt, providerFailure: enOpt(PROVIDER_FAILURES),
361
+ nativeMarker: { t: 'native_marker', optional: true }, reportExpected: boolOpt, providerFailure: enOpt(PROVIDER_FAILURES),
362
362
  },
363
363
  transcript_final: {
364
364
  selfAttributing: bool, nativeRead: bool, nativeMarker: { t: 'native_marker', optional: true },
@@ -440,11 +440,12 @@ function isLiveTurnPending(value: unknown): value is LiveTurnPending {
440
440
  && (value.newestActivityAt === undefined || isFiniteNumber(value.newestActivityAt))
441
441
  }
442
442
 
443
- function isNativeMarkerRef(value: unknown): value is NativeTurnMarkerRef {
443
+ export function isNativeMarkerRef(value: unknown): value is NativeTurnMarkerRef {
444
444
  return isRecord(value)
445
- && hasOnlyKeys(value, ['outcome', 'turnId'])
445
+ && hasOnlyKeys(value, ['outcome', 'turnId', 'receivedAt', 'startedAt', 'errorAt'])
446
446
  && (NATIVE_TURN_OUTCOMES as readonly string[]).includes(value.outcome as string)
447
447
  && (value.turnId === undefined || isEvidenceIdentifier(value.turnId))
448
+ && ['receivedAt', 'startedAt', 'errorAt'].every((key) => value[key] === undefined || isFiniteNumber(value[key]))
448
449
  }
449
450
 
450
451
  function isCoordinatorIdentity(value: unknown): value is EvidenceCoordinatorIdentity {
package/src/types.ts CHANGED
@@ -137,6 +137,7 @@ export interface MeshNodeIdentified {
137
137
  }
138
138
 
139
139
  export interface RepoMeshSessionStatus {
140
+ providerApproval?: import('./provider-approval').ProviderApproval | null;
140
141
  sessionId: string
141
142
  providerType?: string
142
143
  state?: string