@adhdev/mesh-shared 1.0.82-rc.33 → 1.0.82-rc.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistant-protocol.d.ts +13 -2
- package/dist/decision-push-relay.d.ts +20 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +531 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +489 -13
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-tool-names.d.ts +2 -2
- package/dist/mission-items.d.ts +202 -0
- package/dist/protocol/daemon-server.d.ts +33 -2
- package/dist/protocol/dashboard-server.d.ts +10 -2
- package/dist/relay-reprobe.d.ts +52 -0
- package/dist/turn-ipc-mission.d.ts +59 -1
- package/dist/worker-protocol.d.ts +1 -1
- package/package.json +1 -1
- package/src/assistant-protocol.ts +39 -2
- package/src/decision-push-relay.ts +23 -0
- package/src/index.ts +3 -0
- package/src/mesh-tool-names.ts +3 -0
- package/src/mission-items.ts +421 -0
- package/src/protocol/daemon-server.ts +32 -2
- package/src/protocol/dashboard-server.ts +7 -2
- package/src/relay-reprobe.ts +117 -0
- package/src/turn-ipc-mission.ts +108 -9
- package/src/worker-protocol.ts +11 -0
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* Order mirrors mcp-server `ALL_MESH_TOOLS` for easy visual diffing, but the consistency
|
|
19
19
|
* checks are set-based (order-insensitive).
|
|
20
20
|
*/
|
|
21
|
-
export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_queue_revise", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots", "driver_list", "driver_describe", "artifact_list"];
|
|
21
|
+
export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_queue_revise", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_reattach_source_parent", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots", "driver_list", "driver_describe", "artifact_list", "artifact_read", "artifact_register"];
|
|
22
22
|
export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
|
|
23
23
|
/** The count the `NN tools` barrel doc comments and consistency test assert against. */
|
|
24
|
-
export declare const CANONICAL_MESH_TOOL_COUNT:
|
|
24
|
+
export declare const CANONICAL_MESH_TOOL_COUNT: 48;
|
|
25
25
|
/**
|
|
26
26
|
* Tool names retired by the 2026-09-26 tool-surface consolidation (60 → 48).
|
|
27
27
|
* Every capability behind them is still reachable: each retired name maps to the
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mission-items — structured mission records: the item kinds a mission carries
|
|
3
|
+
* (remaining work, live checks, owner decisions, evidence), their normalization
|
|
4
|
+
* and limits, dedupe-key derivation and the close outcome enum
|
|
5
|
+
* (docs/design/2026-10-28-structured-missions-and-owner-decisions.md §4, §5).
|
|
6
|
+
*
|
|
7
|
+
* Philosophy (owner 2026-10-28, design §0): NEVER reject for size or shape —
|
|
8
|
+
* over-long text is truncated, extra refs are dropped, unusable items are
|
|
9
|
+
* reported per-item. The only hard rule lives elsewhere (the daemon): a
|
|
10
|
+
* coordinator or worker can never ANSWER a decision, so `answer`/`delivery`
|
|
11
|
+
* are stripped from every caller-supplied `decision` here.
|
|
12
|
+
*
|
|
13
|
+
* Dependency-free leaf: pure functions over plain data, no storage.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MISSION_ITEM_KINDS: readonly ["remaining", "live_check", "decision", "evidence"];
|
|
16
|
+
export type MissionItemKind = typeof MISSION_ITEM_KINDS[number];
|
|
17
|
+
export declare const MISSION_ITEM_STATES: readonly ["open", "done", "dropped", "moved", "answered", "expired", "withdrawn", "superseded"];
|
|
18
|
+
export type MissionItemState = typeof MISSION_ITEM_STATES[number];
|
|
19
|
+
/** States a coordinator/worker may SET through items_update, per kind. `answered`/`expired` are daemon-owned. */
|
|
20
|
+
export declare const COORDINATOR_SETTABLE_ITEM_STATES: Readonly<Record<MissionItemKind, readonly MissionItemState[]>>;
|
|
21
|
+
export declare const MISSION_OUTCOMES: readonly ["merged", "verified_live", "superseded", "not_needed", "owner_abandoned", "unspecified"];
|
|
22
|
+
export type MissionOutcome = typeof MISSION_OUTCOMES[number];
|
|
23
|
+
export declare function isMissionOutcome(value: unknown): value is MissionOutcome;
|
|
24
|
+
export declare const MISSION_REF_KINDS: readonly ["commit", "branch", "pr", "task", "artifact", "mission", "url"];
|
|
25
|
+
export type MissionRefKind = typeof MISSION_REF_KINDS[number];
|
|
26
|
+
export declare const MISSION_LIMITS: {
|
|
27
|
+
readonly summary: 300;
|
|
28
|
+
readonly outcomeReason: 500;
|
|
29
|
+
readonly itemText: 300;
|
|
30
|
+
readonly machine: 80;
|
|
31
|
+
readonly refs: 8;
|
|
32
|
+
readonly refValue: 300;
|
|
33
|
+
readonly refRepo: 120;
|
|
34
|
+
readonly refNote: 200;
|
|
35
|
+
readonly dedupeKey: 400;
|
|
36
|
+
readonly question: 300;
|
|
37
|
+
readonly context: 600;
|
|
38
|
+
readonly options: 5;
|
|
39
|
+
readonly optionLabel: 60;
|
|
40
|
+
readonly optionDetail: 200;
|
|
41
|
+
/** Open items per mission before the oldest `remaining` fold into a "N more" summary item. */
|
|
42
|
+
readonly openItemsPerMission: 200;
|
|
43
|
+
/** One event payload (serialized). */
|
|
44
|
+
readonly eventPayloadBytes: number;
|
|
45
|
+
/** items_add / items_update entries honoured per call (extra entries are reported, not applied). */
|
|
46
|
+
readonly itemsPerCall: 50;
|
|
47
|
+
};
|
|
48
|
+
export declare const MISSION_DECISION_OPTION_IDS: readonly ["a", "b", "c", "d", "e"];
|
|
49
|
+
export declare const MISSION_DECISION_URGENCIES: readonly ["normal", "blocking"];
|
|
50
|
+
export type MissionDecisionUrgency = typeof MISSION_DECISION_URGENCIES[number];
|
|
51
|
+
export declare const MISSION_DECISION_ON_DUE: readonly ["remind", "apply_recommended"];
|
|
52
|
+
export type MissionDecisionOnDue = typeof MISSION_DECISION_ON_DUE[number];
|
|
53
|
+
export interface MissionItemRef {
|
|
54
|
+
kind: MissionRefKind;
|
|
55
|
+
repo?: string;
|
|
56
|
+
value: string;
|
|
57
|
+
note?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface MissionDecisionOption {
|
|
60
|
+
id: string;
|
|
61
|
+
label: string;
|
|
62
|
+
detail?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Who/where an answer came from. Written only by the daemon's (owner-side) answer path — never from tool input. */
|
|
65
|
+
export interface MissionDecisionAnswer {
|
|
66
|
+
optionId?: string;
|
|
67
|
+
text?: string;
|
|
68
|
+
by: 'owner' | 'auto_recommended';
|
|
69
|
+
via: 'dashboard' | 'assistant' | 'push' | 'cli' | 'auto';
|
|
70
|
+
interpretedFrom?: string;
|
|
71
|
+
at: string;
|
|
72
|
+
ingress?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface MissionDecisionDelivery {
|
|
75
|
+
messageId: string;
|
|
76
|
+
/**
|
|
77
|
+
* `queued`: the coordinator's input queue accepted it but has not typed it yet — the daemon
|
|
78
|
+
* confirms it against the send receipt (a daemon restart drops that queue). Daemon-written.
|
|
79
|
+
*/
|
|
80
|
+
state: 'pending' | 'queued' | 'sent' | 'acked';
|
|
81
|
+
lastAttemptAt?: string;
|
|
82
|
+
/** Coordinator session + funnel messageId of a `queued` delivery (receipt lookup). Daemon-written. */
|
|
83
|
+
sessionId?: string;
|
|
84
|
+
wireMessageId?: string;
|
|
85
|
+
/** Failed send attempts of this revision (retry backoff). Daemon-written. */
|
|
86
|
+
attempts?: number;
|
|
87
|
+
/** What the coordinator is told: a first answer, a changed answer, or an undone one. Daemon-written. */
|
|
88
|
+
kind?: 'answered' | 'changed' | 'undone';
|
|
89
|
+
}
|
|
90
|
+
export interface MissionDecision {
|
|
91
|
+
question: string;
|
|
92
|
+
context?: string;
|
|
93
|
+
options: MissionDecisionOption[];
|
|
94
|
+
recommended?: string;
|
|
95
|
+
allowFreeText: boolean;
|
|
96
|
+
dueAt?: string;
|
|
97
|
+
onDue: MissionDecisionOnDue;
|
|
98
|
+
urgency: MissionDecisionUrgency;
|
|
99
|
+
answer?: MissionDecisionAnswer;
|
|
100
|
+
delivery?: MissionDecisionDelivery;
|
|
101
|
+
/**
|
|
102
|
+
* Coordinator-prompt watermark (bundle B): ISO time the coordinator last "saw" this item's
|
|
103
|
+
* answer. An answered decision is shown in the coordinator prompt while this is absent or
|
|
104
|
+
* older than `answer.at` (a changed answer re-surfaces). Written only by the daemon.
|
|
105
|
+
*/
|
|
106
|
+
promptAckedAt?: string;
|
|
107
|
+
/** Counts answer writes (answer / change / undo); delivery messageIds are `decision:<itemId>:r<n>`. Daemon-written. */
|
|
108
|
+
answerRevision?: number;
|
|
109
|
+
/** ISO time an `onDue: remind` crossing was recorded (once per dueAt). Daemon-written. */
|
|
110
|
+
remindedAt?: string;
|
|
111
|
+
}
|
|
112
|
+
export interface NormalizedMissionItem {
|
|
113
|
+
kind: MissionItemKind;
|
|
114
|
+
text: string;
|
|
115
|
+
machine?: string;
|
|
116
|
+
refs: MissionItemRef[];
|
|
117
|
+
decision?: MissionDecision;
|
|
118
|
+
dedupeKey: string;
|
|
119
|
+
/** Fields that were truncated/dropped — informational, never an error. */
|
|
120
|
+
truncated: string[];
|
|
121
|
+
}
|
|
122
|
+
export declare function truncateMissionText(value: string, max: number): string;
|
|
123
|
+
export declare function normalizeMissionSummary(value: unknown): {
|
|
124
|
+
value: string;
|
|
125
|
+
truncated: boolean;
|
|
126
|
+
} | null;
|
|
127
|
+
export declare function normalizeOutcomeReason(value: unknown): {
|
|
128
|
+
value: string;
|
|
129
|
+
truncated: boolean;
|
|
130
|
+
} | null;
|
|
131
|
+
/** Refs: ≤8, `{kind, repo?, value, note?}`. Unusable entries are dropped; the 9th and later are dropped. */
|
|
132
|
+
export declare function normalizeMissionRefs(value: unknown): {
|
|
133
|
+
refs: MissionItemRef[];
|
|
134
|
+
truncated: boolean;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Caller-supplied decision → stored shape. `answer`/`delivery` (and any other
|
|
138
|
+
* unknown key) are DROPPED: a coordinator/worker cannot pre-answer its own
|
|
139
|
+
* question. `recommended` must name one of the options, else it is dropped.
|
|
140
|
+
*/
|
|
141
|
+
export declare function normalizeDecisionInput(raw: Record<string, unknown>, truncated: string[]): MissionDecision | null;
|
|
142
|
+
/** Normalized text for dedupe: lower-case, whitespace-collapsed, trailing punctuation trimmed. */
|
|
143
|
+
export declare function normalizeForDedupe(text: string): string;
|
|
144
|
+
export declare function deriveItemDedupeKey(kind: MissionItemKind, parts: {
|
|
145
|
+
text?: string;
|
|
146
|
+
machine?: string;
|
|
147
|
+
refs?: readonly MissionItemRef[];
|
|
148
|
+
question?: string;
|
|
149
|
+
explicit?: string;
|
|
150
|
+
}): string;
|
|
151
|
+
/**
|
|
152
|
+
* Normalize one caller-supplied `items_add[]` entry. Returns `null` + `error`
|
|
153
|
+
* only when nothing usable remains (no text/question) — everything else is
|
|
154
|
+
* clamped, never rejected.
|
|
155
|
+
*/
|
|
156
|
+
export declare function normalizeMissionItemInput(raw: unknown): {
|
|
157
|
+
item: NormalizedMissionItem;
|
|
158
|
+
} | {
|
|
159
|
+
error: string;
|
|
160
|
+
};
|
|
161
|
+
export interface MissionItemUpdateInput {
|
|
162
|
+
id: string;
|
|
163
|
+
state?: MissionItemState;
|
|
164
|
+
text?: string;
|
|
165
|
+
machine?: string | null;
|
|
166
|
+
refs?: MissionItemRef[];
|
|
167
|
+
}
|
|
168
|
+
/** Normalize one `items_update[]` entry (id required). The kind-specific state rule is applied by the daemon, which knows the stored kind. */
|
|
169
|
+
export declare function normalizeMissionItemUpdate(raw: unknown): {
|
|
170
|
+
update: MissionItemUpdateInput;
|
|
171
|
+
truncated: string[];
|
|
172
|
+
} | {
|
|
173
|
+
error: string;
|
|
174
|
+
};
|
|
175
|
+
/** Cap a serialized event payload at {@link MISSION_LIMITS.eventPayloadBytes}; over-size payloads become a stub that says so. */
|
|
176
|
+
export declare function boundEventPayload(payload: Record<string, unknown>): string;
|
|
177
|
+
/** The carry-over mission's fixed title (design §5.1). */
|
|
178
|
+
export declare const CARRY_OVER_MISSION_TITLE = "Carry-over \u2014 open items from closed missions";
|
|
179
|
+
/** The title earlier builds stored; existing DBs still hold it, so it keeps matching (no rename). */
|
|
180
|
+
export declare const LEGACY_CARRY_OVER_MISSION_TITLE = "\uC774\uC6D4 \u2014 \uB2EB\uD78C \uBBF8\uC158\uC5D0\uC11C \uB118\uC5B4\uC628 \uD56D\uBAA9";
|
|
181
|
+
/** Canonical goal text of a newly created carry-over mission. */
|
|
182
|
+
export declare const CARRY_OVER_MISSION_GOAL = "Open items carried over from closed missions, and decisions requested without a mission. Managed automatically \u2014 do not close.";
|
|
183
|
+
/**
|
|
184
|
+
* THE one predicate for "is this the auto carry-over mission". The daemon's `is_carry_over`
|
|
185
|
+
* column (`isCarryOver`) is the source of truth; wire views that cannot carry the flag (older
|
|
186
|
+
* strict decoders reject unknown keys) fall back to the fixed title. Use this everywhere.
|
|
187
|
+
*/
|
|
188
|
+
export declare function isCarryOverMissionRecord(m: {
|
|
189
|
+
isCarryOver?: boolean;
|
|
190
|
+
title?: unknown;
|
|
191
|
+
} | null | undefined): boolean;
|
|
192
|
+
/** The carry-over mission's title is fixed (the daemon ignores renames), so a title match identifies it on wire views that carry no flag. */
|
|
193
|
+
export declare function isCarryOverMissionTitle(title: unknown): boolean;
|
|
194
|
+
/** Prefix of the owner-confirmation decision a coordinator's `owner_abandoned` close creates. */
|
|
195
|
+
export declare const OWNER_ABANDON_CONFIRM_KEY_PREFIX = "owner-abandon-confirm:";
|
|
196
|
+
export declare const OWNER_ABANDON_CONFIRM_QUESTION = "The coordinator closed this as 'owner abandoned' \u2014 correct?";
|
|
197
|
+
export declare const OWNER_ABANDON_CONFIRM_OPTIONS: readonly ["Correct", "Reopen"];
|
|
198
|
+
/** worker `report_completion.decisions_needed` cap. */
|
|
199
|
+
export declare const WORKER_DECISIONS_NEEDED_MAX = 3;
|
|
200
|
+
/** worker `report_completion.commits` cap and shape. */
|
|
201
|
+
export declare const WORKER_COMMITS_MAX = 20;
|
|
202
|
+
export declare function normalizeReportCommits(value: unknown): string[] | null;
|
|
@@ -32,15 +32,38 @@ import { type CommandPayload, type Envelope, type MessageId } from './envelope';
|
|
|
32
32
|
* time (UserSession.ts), so adding a member here without adding it there — or
|
|
33
33
|
* vice versa — fails `tsc`.
|
|
34
34
|
*/
|
|
35
|
-
export declare const DAEMON_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress"];
|
|
35
|
+
export declare const DAEMON_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress", "agent:decision_needed"];
|
|
36
36
|
export type DaemonStatusEventName = typeof DAEMON_STATUS_EVENT_NAMES[number];
|
|
37
|
-
export declare const isDaemonStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress";
|
|
37
|
+
export declare const isDaemonStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress" | "agent:decision_needed";
|
|
38
38
|
/**
|
|
39
39
|
* Server-bound status event. Content boundary: identifiers, enums, counters
|
|
40
40
|
* and booleans only — plus the approved approval-modal exception
|
|
41
41
|
* (`modalMessage`/`modalButtons`, CLAUDE.md "Push notification exception").
|
|
42
42
|
* Mirror of daemon-core `DaemonStatusEventPayload`.
|
|
43
43
|
*/
|
|
44
|
+
/**
|
|
45
|
+
* Owner-decision push payload (design §9, owner decision D1 2026-10-28). The ONLY mission text that
|
|
46
|
+
* reaches the server: the question (<=80 chars) and up to 3 option labels (<=24 chars). Never
|
|
47
|
+
* `context`, free-text answers, mission goal/summary or chat content.
|
|
48
|
+
*/
|
|
49
|
+
export type DecisionPushWire = {
|
|
50
|
+
meshId: string;
|
|
51
|
+
missionId: string;
|
|
52
|
+
itemId: string;
|
|
53
|
+
question: string;
|
|
54
|
+
options: Array<{
|
|
55
|
+
id: string;
|
|
56
|
+
label: string;
|
|
57
|
+
}>;
|
|
58
|
+
recommended?: string;
|
|
59
|
+
urgency: 'normal' | 'blocking';
|
|
60
|
+
dueAt?: string;
|
|
61
|
+
/** `new` = freshly asked; `remind` = a blocking decision crossed its due time. */
|
|
62
|
+
reason: 'new' | 'remind';
|
|
63
|
+
};
|
|
64
|
+
export declare const DECISION_PUSH_QUESTION_MAX = 80;
|
|
65
|
+
export declare const DECISION_PUSH_OPTION_LABEL_MAX = 24;
|
|
66
|
+
export declare const DECISION_PUSH_OPTIONS_MAX = 3;
|
|
44
67
|
export type DaemonStatusEventWirePayload = {
|
|
45
68
|
event: DaemonStatusEventName;
|
|
46
69
|
timestamp: number;
|
|
@@ -57,6 +80,8 @@ export type DaemonStatusEventWirePayload = {
|
|
|
57
80
|
waitKind?: 'external';
|
|
58
81
|
surfaceHidden?: boolean;
|
|
59
82
|
muted?: boolean;
|
|
83
|
+
/** Present only on `agent:decision_needed`. */
|
|
84
|
+
decision?: DecisionPushWire;
|
|
60
85
|
};
|
|
61
86
|
/** Routing-only session entry the daemon reports over the server WS. Mirror of daemon-core `RoutingSessionEntry`. */
|
|
62
87
|
export type RoutingSessionWireEntry = {
|
|
@@ -218,6 +243,7 @@ export type P2POfferWirePayload = {
|
|
|
218
243
|
peerId: string;
|
|
219
244
|
attemptId?: number;
|
|
220
245
|
attemptEpoch?: string;
|
|
246
|
+
probe?: boolean;
|
|
221
247
|
};
|
|
222
248
|
export type P2PIceWirePayload = {
|
|
223
249
|
candidate: string;
|
|
@@ -225,6 +251,7 @@ export type P2PIceWirePayload = {
|
|
|
225
251
|
peerId: string;
|
|
226
252
|
attemptId?: number;
|
|
227
253
|
attemptEpoch?: string;
|
|
254
|
+
probe?: boolean;
|
|
228
255
|
};
|
|
229
256
|
/**
|
|
230
257
|
* A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
|
|
@@ -238,6 +265,10 @@ export type RelayedP2PSignalWirePayload = {
|
|
|
238
265
|
daemonId?: string;
|
|
239
266
|
attemptId?: number;
|
|
240
267
|
attemptEpoch?: string;
|
|
268
|
+
/** p2p_ready only: the dashboard's stale-connect backoff index (see DashboardP2PReadyWirePayload). */
|
|
269
|
+
backoffStep?: number;
|
|
270
|
+
/** Make-before-break relay->direct re-probe of an existing peer (see DashboardP2PReadyWirePayload). */
|
|
271
|
+
probe?: boolean;
|
|
241
272
|
sharePermission?: string;
|
|
242
273
|
shareTargetSessionId?: string;
|
|
243
274
|
sdp?: string;
|
|
@@ -21,9 +21,9 @@ import { type DaemonStatusEventName, type P2PIceWirePayload, type P2POfferWirePa
|
|
|
21
21
|
/** Server-originated events the dashboard receives in addition to the daemon set. */
|
|
22
22
|
export declare const SERVER_STATUS_EVENT_NAMES: readonly ["daemon:disconnect", "team:session_viewed", "team:view_request", "team:view_request_approved", "team:view_request_rejected"];
|
|
23
23
|
/** Mirror of daemon-core `DashboardStatusEventName`; pinned equal by the server at compile time. */
|
|
24
|
-
export declare const DASHBOARD_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress", "daemon:disconnect", "team:session_viewed", "team:view_request", "team:view_request_approved", "team:view_request_rejected"];
|
|
24
|
+
export declare const DASHBOARD_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress", "agent:decision_needed", "daemon:disconnect", "team:session_viewed", "team:view_request", "team:view_request_approved", "team:view_request_rejected"];
|
|
25
25
|
export type DashboardStatusEventName = DaemonStatusEventName | typeof SERVER_STATUS_EVENT_NAMES[number];
|
|
26
|
-
export declare const isDashboardStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress" | "daemon:disconnect" | "team:session_viewed" | "team:view_request" | "team:view_request_approved" | "team:view_request_rejected";
|
|
26
|
+
export declare const isDashboardStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress" | "agent:decision_needed" | "daemon:disconnect" | "team:session_viewed" | "team:view_request" | "team:view_request_approved" | "team:view_request_rejected";
|
|
27
27
|
/**
|
|
28
28
|
* Sanitized status event as the server relays it (UserSession
|
|
29
29
|
* `buildDashboardStatusEvent`). Mirror of daemon-core
|
|
@@ -111,6 +111,7 @@ export type DashboardP2POfferWirePayload = {
|
|
|
111
111
|
peerId?: string;
|
|
112
112
|
attemptId?: number;
|
|
113
113
|
attemptEpoch?: string;
|
|
114
|
+
probe?: boolean;
|
|
114
115
|
sdp: string;
|
|
115
116
|
type: string;
|
|
116
117
|
};
|
|
@@ -119,6 +120,7 @@ export type DashboardP2PIceWirePayload = {
|
|
|
119
120
|
peerId?: string;
|
|
120
121
|
attemptId?: number;
|
|
121
122
|
attemptEpoch?: string;
|
|
123
|
+
probe?: boolean;
|
|
122
124
|
candidate: string;
|
|
123
125
|
mid?: string;
|
|
124
126
|
sdpMid?: string | null;
|
|
@@ -155,11 +157,15 @@ export declare const isDashboardToUserSessionType: (value: unknown) => value is
|
|
|
155
157
|
export declare const DASHBOARD_P2P_SIGNAL_TYPES: readonly ["p2p_ready", "p2p_answer", "p2p_ice"];
|
|
156
158
|
export type DashboardP2PSignalType = typeof DASHBOARD_P2P_SIGNAL_TYPES[number];
|
|
157
159
|
export declare const isDashboardP2PSignalType: (value: unknown) => value is "p2p_ready" | "p2p_answer" | "p2p_ice";
|
|
160
|
+
/** `backoffStep`: the dashboard's stale-connect backoff index (0 = 15s, 1 = 30s, 2 = 60s), so the daemon holds the connecting slot for the same window. */
|
|
161
|
+
/** `probe`: identifier-only flag — this signal belongs to a make-before-break relay->direct re-probe of the SAME logical peer (never a new peer for the connection cap). */
|
|
158
162
|
export type DashboardP2PReadyWirePayload = {
|
|
159
163
|
daemonId: string;
|
|
160
164
|
peerId: string;
|
|
161
165
|
attemptId?: number;
|
|
162
166
|
attemptEpoch?: string;
|
|
167
|
+
backoffStep?: number;
|
|
168
|
+
probe?: boolean;
|
|
163
169
|
};
|
|
164
170
|
export type DashboardP2PAnswerWirePayload = {
|
|
165
171
|
daemonId: string;
|
|
@@ -168,12 +174,14 @@ export type DashboardP2PAnswerWirePayload = {
|
|
|
168
174
|
type: string;
|
|
169
175
|
attemptId?: number;
|
|
170
176
|
attemptEpoch?: string;
|
|
177
|
+
probe?: boolean;
|
|
171
178
|
};
|
|
172
179
|
export type DashboardP2PLocalIceWirePayload = {
|
|
173
180
|
daemonId: string;
|
|
174
181
|
peerId: string;
|
|
175
182
|
attemptId?: number;
|
|
176
183
|
attemptEpoch?: string;
|
|
184
|
+
probe?: boolean;
|
|
177
185
|
candidate: string;
|
|
178
186
|
sdpMid: string | null;
|
|
179
187
|
sdpMLineIndex: number | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay -> direct re-probe scheduler (pure, dependency-free).
|
|
3
|
+
*
|
|
4
|
+
* A P2P connection that settled on a TURN relay never looks for a direct path
|
|
5
|
+
* again. The owning transport (dashboard `P2PConnection`, daemon peer manager)
|
|
6
|
+
* uses this scheduler to periodically attempt a make-before-break probe: a
|
|
7
|
+
* SECOND peer connection (STUN-only) is built next to the working relay one and
|
|
8
|
+
* only swapped in once it is verified direct. The scheduler owns nothing but
|
|
9
|
+
* timing: backoff 60s -> 5min (doubling), jitter, and the stop conditions. The
|
|
10
|
+
* transport supplies `shouldProbe` (open + idle + visible) and `probe`.
|
|
11
|
+
*/
|
|
12
|
+
export type RelayReprobeOutcome = 'direct' | 'relay' | 'failed' | 'skipped';
|
|
13
|
+
export interface RelayReprobeTiming {
|
|
14
|
+
/** First delay and the unit of growth (default 60s). */
|
|
15
|
+
baseDelayMs?: number;
|
|
16
|
+
/** Backoff ceiling before jitter (default 5min). */
|
|
17
|
+
maxDelayMs?: number;
|
|
18
|
+
/** +/- fraction applied to every delay (default 0.2). */
|
|
19
|
+
jitterRatio?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const RELAY_REPROBE_BASE_DELAY_MS = 60000;
|
|
22
|
+
export declare const RELAY_REPROBE_MAX_DELAY_MS: number;
|
|
23
|
+
export declare const RELAY_REPROBE_JITTER_RATIO = 0.2;
|
|
24
|
+
/** Delay before the next attempt after `failures` consecutive non-direct outcomes. */
|
|
25
|
+
export declare function computeRelayReprobeDelay(failures: number, random: () => number, timing?: RelayReprobeTiming): number;
|
|
26
|
+
export interface RelayReprobeSchedulerOptions extends RelayReprobeTiming {
|
|
27
|
+
random: () => number;
|
|
28
|
+
setTimer: (callback: () => void, ms: number) => unknown;
|
|
29
|
+
clearTimer: (handle: unknown) => void;
|
|
30
|
+
/** Connected over relay, idle, tab visible, auth epoch unchanged... Re-checked on every tick. */
|
|
31
|
+
shouldProbe: () => boolean;
|
|
32
|
+
/** One make-before-break attempt. Must never throw into the scheduler; a rejection counts as 'failed'. */
|
|
33
|
+
probe: () => Promise<RelayReprobeOutcome>;
|
|
34
|
+
}
|
|
35
|
+
export declare class RelayReprobeScheduler {
|
|
36
|
+
private readonly options;
|
|
37
|
+
private handle;
|
|
38
|
+
private running;
|
|
39
|
+
private inFlight;
|
|
40
|
+
private failureCount;
|
|
41
|
+
private generation;
|
|
42
|
+
constructor(options: RelayReprobeSchedulerOptions);
|
|
43
|
+
get isRunning(): boolean;
|
|
44
|
+
get failures(): number;
|
|
45
|
+
get probing(): boolean;
|
|
46
|
+
/** Begin probing (idempotent). Call when a connection is selected as relay. */
|
|
47
|
+
start(): void;
|
|
48
|
+
/** Stop for good (direct path found, connection closed, auth epoch changed). */
|
|
49
|
+
stop(): void;
|
|
50
|
+
private schedule;
|
|
51
|
+
private tick;
|
|
52
|
+
}
|
|
@@ -30,8 +30,40 @@ export interface MissionUpsertRequest {
|
|
|
30
30
|
* this wire guard only checks SHAPE, not that decision.
|
|
31
31
|
*/
|
|
32
32
|
brief?: MissionBriefWire | null;
|
|
33
|
+
/**
|
|
34
|
+
* Structured-missions bundle A (design 2026-10-28 §5.1). Every key below is optional
|
|
35
|
+
* and ABSENT on a legacy call. `structured: true` is the caller's capability flag: the
|
|
36
|
+
* daemon only puts the new keys on the response (mission summary/outcome/version,
|
|
37
|
+
* `result`) when it is set, so an older mcp-server that never sends it keeps decoding
|
|
38
|
+
* byte-identical responses (mixed-version fleets, design §13.1).
|
|
39
|
+
*/
|
|
40
|
+
structured?: boolean;
|
|
41
|
+
summary?: string;
|
|
42
|
+
outcome?: string;
|
|
43
|
+
outcomeReason?: string;
|
|
44
|
+
/** Mission id open items move to when this call closes the mission (default: the carry-over mission). */
|
|
45
|
+
carryOverTo?: string;
|
|
46
|
+
/** Route to the mesh's auto-created carry-over mission instead of `id`/`title` (decision requests without a mission). `title` may then be empty. */
|
|
47
|
+
toCarryOver?: boolean;
|
|
48
|
+
itemsAdd?: readonly Record<string, unknown>[];
|
|
49
|
+
itemsUpdate?: readonly Record<string, unknown>[];
|
|
50
|
+
}
|
|
51
|
+
export interface MeshMissionOpenItemCountsWire {
|
|
52
|
+
remaining: number;
|
|
53
|
+
live_check: number;
|
|
54
|
+
decision: number;
|
|
55
|
+
evidence: number;
|
|
56
|
+
}
|
|
57
|
+
/** Structured-missions fields (only with `structured: true` on the request). */
|
|
58
|
+
export interface MeshMissionStructuredWire {
|
|
59
|
+
summary?: string;
|
|
60
|
+
outcome?: string;
|
|
61
|
+
outcomeReason?: string;
|
|
62
|
+
version?: number;
|
|
63
|
+
carryOver?: boolean;
|
|
64
|
+
openItems?: MeshMissionOpenItemCountsWire;
|
|
33
65
|
}
|
|
34
|
-
export interface MeshMissionRecordWire {
|
|
66
|
+
export interface MeshMissionRecordWire extends MeshMissionStructuredWire {
|
|
35
67
|
id: string;
|
|
36
68
|
meshId: string;
|
|
37
69
|
title: string;
|
|
@@ -42,6 +74,8 @@ export interface MeshMissionRecordWire {
|
|
|
42
74
|
}
|
|
43
75
|
export interface MissionUpsertResponse {
|
|
44
76
|
mission: MeshMissionRecordWire;
|
|
77
|
+
/** Structured calls only: per-item results, movedItems/movedTo, restoredItems, ownerConfirmationItemId, warnings. */
|
|
78
|
+
result?: Record<string, unknown>;
|
|
45
79
|
}
|
|
46
80
|
export declare function decodeMissionUpsertRequest(value: unknown): MissionUpsertRequest | null;
|
|
47
81
|
export declare function isMissionUpsertResponse(value: unknown): value is MissionUpsertResponse;
|
|
@@ -77,6 +111,12 @@ export interface MissionListQueryRequest {
|
|
|
77
111
|
* option except `historyIdLimit` is ignored. An older daemon rejects the key.
|
|
78
112
|
*/
|
|
79
113
|
meshStatusView?: 'compact' | 'verbose';
|
|
114
|
+
/** Structured-missions capability flag (see MissionUpsertRequest.structured): rows then carry summary/outcome/openItems/carryOver/version. */
|
|
115
|
+
structured?: boolean;
|
|
116
|
+
/** Structured only: attach each mission's items. */
|
|
117
|
+
items?: boolean;
|
|
118
|
+
/** Structured only: attach each mission's change history (bounded tail). */
|
|
119
|
+
history?: boolean;
|
|
80
120
|
}
|
|
81
121
|
export interface MeshMissionTaskAggregateWire {
|
|
82
122
|
total: number;
|
|
@@ -115,6 +155,15 @@ export interface MissionListSummaryVerboseWire {
|
|
|
115
155
|
createdAt?: string;
|
|
116
156
|
updatedAt?: string;
|
|
117
157
|
closeCandidateEmittedAt?: string | null;
|
|
158
|
+
/** Structured lists only. */
|
|
159
|
+
summary?: string;
|
|
160
|
+
outcome?: string;
|
|
161
|
+
outcomeReason?: string;
|
|
162
|
+
version?: number;
|
|
163
|
+
carryOver?: boolean;
|
|
164
|
+
openItems?: MeshMissionOpenItemCountsWire;
|
|
165
|
+
items?: readonly Record<string, unknown>[];
|
|
166
|
+
history?: readonly Record<string, unknown>[];
|
|
118
167
|
}
|
|
119
168
|
/** Compact (default) mission row. `goalPreview`/`goalTruncated` present, `goal` absent. */
|
|
120
169
|
export interface MissionListSummarySlimWire {
|
|
@@ -132,6 +181,15 @@ export interface MissionListSummarySlimWire {
|
|
|
132
181
|
createdAt?: string;
|
|
133
182
|
updatedAt?: string;
|
|
134
183
|
closeCandidateEmittedAt?: string | null;
|
|
184
|
+
/** Structured lists only. */
|
|
185
|
+
summary?: string;
|
|
186
|
+
outcome?: string;
|
|
187
|
+
outcomeReason?: string;
|
|
188
|
+
version?: number;
|
|
189
|
+
carryOver?: boolean;
|
|
190
|
+
openItems?: MeshMissionOpenItemCountsWire;
|
|
191
|
+
items?: readonly Record<string, unknown>[];
|
|
192
|
+
history?: readonly Record<string, unknown>[];
|
|
135
193
|
}
|
|
136
194
|
export type MissionListSummaryWire = MissionListSummaryVerboseWire | MissionListSummarySlimWire;
|
|
137
195
|
export interface MeshMissionHistoryFoldWire {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* the protocol contract the marker demarcates.
|
|
31
31
|
*/
|
|
32
32
|
import { type MissionBrief } from './mission-brief';
|
|
33
|
-
export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff", "driver_list", "driver_describe", "driver_invoke", "artifact_register"];
|
|
33
|
+
export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff", "driver_list", "driver_describe", "driver_invoke", "driver_scaffold", "driver_validate", "driver_export", "driver_install", "artifact_register", "artifact_list", "artifact_read", "contract_read", "contract_observe", "contract_current"];
|
|
34
34
|
export type WorkerTool = typeof WORKER_TOOLS[number];
|
|
35
35
|
export declare function isWorkerTool(value: unknown): value is WorkerTool;
|
|
36
36
|
/** Stable marker line so a body is never footered twice and tests can find it. */
|
package/package.json
CHANGED
|
@@ -34,6 +34,10 @@ export const ASSISTANT_TOOLS = [
|
|
|
34
34
|
'launch_defaults_set',
|
|
35
35
|
'driver_list',
|
|
36
36
|
'driver_describe',
|
|
37
|
+
'artifact_list',
|
|
38
|
+
'artifact_read',
|
|
39
|
+
'decision_answer',
|
|
40
|
+
'decision_request',
|
|
37
41
|
] as const
|
|
38
42
|
|
|
39
43
|
export type AssistantTool = typeof ASSISTANT_TOOLS[number]
|
|
@@ -67,6 +71,12 @@ export const ASSISTANT_VERB = {
|
|
|
67
71
|
projectNote: 'assistant_project_note',
|
|
68
72
|
launchDefaults: 'assistant_launch_defaults',
|
|
69
73
|
launchDefaultsSet: 'assistant_launch_defaults_set',
|
|
74
|
+
artifactList: 'assistant_artifact_list',
|
|
75
|
+
artifactRead: 'assistant_artifact_read',
|
|
76
|
+
/** Owner-decision inbox (structured-missions design 2026-10-28 §6.3): the owner's words recorded as answers. */
|
|
77
|
+
decisionAnswer: 'assistant_decision_answer',
|
|
78
|
+
/** Register a plain-text question a coordinator put to the owner as a decision item (§5.2). */
|
|
79
|
+
decisionRequest: 'assistant_decision_request',
|
|
70
80
|
// assistant-side daemon verbs without a tool of the same name
|
|
71
81
|
launch: 'launch_assistant',
|
|
72
82
|
pendingRelays: 'assistant_pending_relays',
|
|
@@ -101,6 +111,10 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
|
|
|
101
111
|
project_note: ASSISTANT_VERB.projectNote,
|
|
102
112
|
launch_defaults: ASSISTANT_VERB.launchDefaults,
|
|
103
113
|
launch_defaults_set: ASSISTANT_VERB.launchDefaultsSet,
|
|
114
|
+
artifact_list: ASSISTANT_VERB.artifactList,
|
|
115
|
+
artifact_read: ASSISTANT_VERB.artifactRead,
|
|
116
|
+
decision_answer: ASSISTANT_VERB.decisionAnswer,
|
|
117
|
+
decision_request: ASSISTANT_VERB.decisionRequest,
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
/**
|
|
@@ -109,7 +123,7 @@ export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>
|
|
|
109
123
|
* because they cause effects (a coordinator message or launch, a new
|
|
110
124
|
* project), and MCP clients use readOnlyHint to skip confirmation.
|
|
111
125
|
*/
|
|
112
|
-
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']
|
|
126
|
+
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']
|
|
113
127
|
|
|
114
128
|
/** Owner-only verbs: never reachable from the assistant's MCP server (no `ipc`). */
|
|
115
129
|
export const ASSISTANT_OWNER_VERBS: readonly AssistantVerb[] = [
|
|
@@ -164,7 +178,7 @@ export const ASSISTANT_REMOTE_FOLLOW_COMMAND = 'assistant_remote_follow' as cons
|
|
|
164
178
|
* `assistant_remote_project` when the host's `assistantRemoteAccess` ACL is active.
|
|
165
179
|
* `note`, `terminal`, and `inventory` remain strictly roster-member-only.
|
|
166
180
|
*/
|
|
167
|
-
export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send'] as const
|
|
181
|
+
export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send', 'artifact_list', 'artifact_read', 'decision_list', 'decision_answer', 'decision_request'] as const
|
|
168
182
|
export type AssistantFollowerAllowedOp = typeof ASSISTANT_FOLLOWER_ALLOWED_OPS[number]
|
|
169
183
|
|
|
170
184
|
const ASSISTANT_FOLLOWER_ALLOWED_OP_SET: ReadonlySet<string> = new Set(ASSISTANT_FOLLOWER_ALLOWED_OPS)
|
|
@@ -173,6 +187,25 @@ export function isAssistantFollowerAllowedOp(value: unknown): value is Assistant
|
|
|
173
187
|
return typeof value === 'string' && ASSISTANT_FOLLOWER_ALLOWED_OP_SET.has(value)
|
|
174
188
|
}
|
|
175
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Owner-decision ops were added after grants were persisted with an explicit
|
|
192
|
+
* `allowedOps` list. A grant that already lets the follower read status (list)
|
|
193
|
+
* or send to the coordinator (answer / register) covers the matching decision op,
|
|
194
|
+
* so no existing grant needs re-issuing.
|
|
195
|
+
*/
|
|
196
|
+
const DECISION_OP_IMPLIED_BY: Readonly<Record<string, AssistantFollowerAllowedOp>> = {
|
|
197
|
+
decision_list: 'status',
|
|
198
|
+
decision_answer: 'send',
|
|
199
|
+
decision_request: 'send',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function followerOpPermitted(op: string, allowedOps: readonly AssistantFollowerAllowedOp[]): boolean {
|
|
203
|
+
if (!isAssistantFollowerAllowedOp(op)) return false
|
|
204
|
+
if ((allowedOps as readonly string[]).includes(op)) return true
|
|
205
|
+
const implied = DECISION_OP_IMPLIED_BY[op]
|
|
206
|
+
return implied !== undefined && allowedOps.includes(implied)
|
|
207
|
+
}
|
|
208
|
+
|
|
176
209
|
/**
|
|
177
210
|
* Host-authoritative narrow project ACL persisted on the hosting daemon's
|
|
178
211
|
* `LocalMeshEntry` in `meshes.json`. Revocation increments `grantRevision`
|
|
@@ -188,6 +221,7 @@ export interface AssistantRemoteAccessBinding {
|
|
|
188
221
|
status: 'active' | 'revoked'
|
|
189
222
|
allowedOps: readonly AssistantFollowerAllowedOp[]
|
|
190
223
|
allowCoordinatorLaunch?: boolean
|
|
224
|
+
allowArtifactRead?: boolean
|
|
191
225
|
grantedAt: number
|
|
192
226
|
revokedAt?: number
|
|
193
227
|
}
|
|
@@ -234,6 +268,7 @@ export interface FollowedRemoteProjectEntry {
|
|
|
234
268
|
bindingRevision: number
|
|
235
269
|
grantRevision: number
|
|
236
270
|
allowCoordinatorLaunch?: boolean
|
|
271
|
+
allowArtifactRead?: boolean
|
|
237
272
|
state: FollowedRemoteProjectState
|
|
238
273
|
followedAt: number | string
|
|
239
274
|
updatedAt: number | string
|
|
@@ -264,6 +299,7 @@ export interface RemoteProjectCatalogItem {
|
|
|
264
299
|
sanitizedRepoIdentity: string | null
|
|
265
300
|
coordinatorState?: 'none' | 'idle' | 'working' | 'waiting' | string
|
|
266
301
|
allowCoordinatorLaunch?: boolean
|
|
302
|
+
allowArtifactRead?: boolean
|
|
267
303
|
capabilities: {
|
|
268
304
|
status: true
|
|
269
305
|
read: true
|
|
@@ -272,6 +308,7 @@ export interface RemoteProjectCatalogItem {
|
|
|
272
308
|
send: true
|
|
273
309
|
coordinatorPresent?: boolean
|
|
274
310
|
allowCoordinatorLaunch?: boolean
|
|
311
|
+
allowArtifactRead?: boolean
|
|
275
312
|
}
|
|
276
313
|
observedAt: number | string
|
|
277
314
|
}
|
|
@@ -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
|
+
}
|