@adhdev/daemon-core 0.9.82-rc.196 → 0.9.82-rc.198
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/index.d.ts +2 -0
- package/dist/index.js +507 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +505 -26
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-delivery-policy.d.ts +126 -0
- package/dist/mesh/mesh-runtime-store.d.ts +66 -0
- package/dist/providers/spec/driver.d.ts +19 -0
- package/dist/providers/spec/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/commands/router.ts +19 -0
- package/src/index.ts +4 -0
- package/src/mesh/mesh-delivery-policy.ts +298 -0
- package/src/mesh/mesh-events.ts +52 -3
- package/src/mesh/mesh-runtime-store.ts +219 -0
- package/src/providers/spec/cli-adapter.ts +4 -0
- package/src/providers/spec/driver.ts +85 -3
- package/src/providers/spec/types.ts +6 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Possible delivery statuses for a session delivery record.
|
|
3
|
+
*/
|
|
4
|
+
export type MeshSessionDeliveryStatus = 'queued' | 'delivering' | 'delivered' | 'acked' | 'completed' | 'failed' | 'expired' | 'cancelled';
|
|
5
|
+
/**
|
|
6
|
+
* Kind of delivery — controls priority and policy handling.
|
|
7
|
+
*/
|
|
8
|
+
export type MeshSessionDeliveryKind = 'task' | 'followup' | 'approval' | 'recovery' | 'system_notice';
|
|
9
|
+
/**
|
|
10
|
+
* A session delivery decision — what to do when a task arrives for a session.
|
|
11
|
+
*/
|
|
12
|
+
export type MeshDeliveryDecision = 'immediate' | 'queued' | 'rejected';
|
|
13
|
+
export interface MeshDeliveryPolicyResult {
|
|
14
|
+
decision: MeshDeliveryDecision;
|
|
15
|
+
reason: string;
|
|
16
|
+
/** When decision='queued', estimated deliver-after ISO timestamp if known. */
|
|
17
|
+
deliverAfter?: string;
|
|
18
|
+
/** Human-readable explanation for coordinator/operator. */
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Determine whether to deliver immediately, queue, or reject based on session status.
|
|
23
|
+
*
|
|
24
|
+
* This is a pure function — it does not write to any store.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveDeliveryDecision(sessionStatus: string | undefined, opts?: {
|
|
27
|
+
kind?: MeshSessionDeliveryKind;
|
|
28
|
+
/** When true, busy session immediate injection is allowed (provider-specific capability). */
|
|
29
|
+
allowBusyInjection?: boolean;
|
|
30
|
+
}): MeshDeliveryPolicyResult;
|
|
31
|
+
export interface SessionDeliveryRecord {
|
|
32
|
+
id: string;
|
|
33
|
+
meshId: string;
|
|
34
|
+
nodeId?: string;
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
providerType?: string;
|
|
37
|
+
taskId?: string;
|
|
38
|
+
kind: MeshSessionDeliveryKind;
|
|
39
|
+
priority: number;
|
|
40
|
+
message: string;
|
|
41
|
+
status: MeshSessionDeliveryStatus;
|
|
42
|
+
deliverAfter?: string;
|
|
43
|
+
expiresAt?: string;
|
|
44
|
+
attemptCount: number;
|
|
45
|
+
sourceCoordinatorSessionId?: string;
|
|
46
|
+
sourceCoordinatorDaemonId?: string;
|
|
47
|
+
lastError?: string;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a delivery record in the store.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createSessionDelivery(opts: {
|
|
55
|
+
meshId: string;
|
|
56
|
+
nodeId?: string;
|
|
57
|
+
sessionId?: string;
|
|
58
|
+
providerType?: string;
|
|
59
|
+
taskId?: string;
|
|
60
|
+
kind: MeshSessionDeliveryKind;
|
|
61
|
+
message: string;
|
|
62
|
+
status: MeshSessionDeliveryStatus;
|
|
63
|
+
priority?: number;
|
|
64
|
+
deliverAfter?: string;
|
|
65
|
+
expiresAt?: string;
|
|
66
|
+
sourceCoordinatorSessionId?: string;
|
|
67
|
+
sourceCoordinatorDaemonId?: string;
|
|
68
|
+
}): SessionDeliveryRecord;
|
|
69
|
+
/**
|
|
70
|
+
* Update the status of a delivery record.
|
|
71
|
+
*/
|
|
72
|
+
export declare function updateSessionDeliveryStatus(id: string, status: MeshSessionDeliveryStatus, opts?: {
|
|
73
|
+
lastError?: string;
|
|
74
|
+
incrementAttempt?: boolean;
|
|
75
|
+
}): void;
|
|
76
|
+
/**
|
|
77
|
+
* Get active (non-terminal) deliveries for a mesh, optionally filtered by session.
|
|
78
|
+
*/
|
|
79
|
+
export declare function getActiveSessionDeliveries(meshId: string, sessionId?: string): {
|
|
80
|
+
id: string;
|
|
81
|
+
meshId: string;
|
|
82
|
+
nodeId: string | null;
|
|
83
|
+
sessionId: string | null;
|
|
84
|
+
providerType: string | null;
|
|
85
|
+
taskId: string | null;
|
|
86
|
+
kind: string;
|
|
87
|
+
priority: number;
|
|
88
|
+
message: string;
|
|
89
|
+
status: string;
|
|
90
|
+
deliverAfter: string | null;
|
|
91
|
+
expiresAt: string | null;
|
|
92
|
+
attemptCount: number;
|
|
93
|
+
sourceCoordinatorSessionId: string | null;
|
|
94
|
+
sourceCoordinatorDaemonId: string | null;
|
|
95
|
+
lastError: string | null;
|
|
96
|
+
createdAt: string;
|
|
97
|
+
updatedAt: string;
|
|
98
|
+
}[];
|
|
99
|
+
/**
|
|
100
|
+
* Record a completion conflict diagnostic when a duplicate event points to
|
|
101
|
+
* different task/session than the already-seen event with the same fingerprint.
|
|
102
|
+
*/
|
|
103
|
+
export declare function recordCompletionConflict(opts: {
|
|
104
|
+
meshId: string;
|
|
105
|
+
fingerprint: string;
|
|
106
|
+
conflictingTaskId?: string;
|
|
107
|
+
conflictingSessionId?: string;
|
|
108
|
+
originalTaskId?: string;
|
|
109
|
+
originalSessionId?: string;
|
|
110
|
+
event: string;
|
|
111
|
+
}): void;
|
|
112
|
+
/**
|
|
113
|
+
* Get recent completion conflicts for diagnostic inspection.
|
|
114
|
+
*/
|
|
115
|
+
export declare function getRecentCompletionConflicts(meshId: string, limitMs?: number): {
|
|
116
|
+
id: string;
|
|
117
|
+
meshId: string;
|
|
118
|
+
fingerprint: string;
|
|
119
|
+
conflictingTaskId: string | null;
|
|
120
|
+
conflictingSessionId: string | null;
|
|
121
|
+
originalTaskId: string | null;
|
|
122
|
+
originalSessionId: string | null;
|
|
123
|
+
event: string;
|
|
124
|
+
createdAt: string;
|
|
125
|
+
}[];
|
|
126
|
+
export declare function __clearSessionDeliveriesForTests(meshId: string): void;
|
|
@@ -80,4 +80,70 @@ export declare class MeshRuntimeStore {
|
|
|
80
80
|
}>;
|
|
81
81
|
deleteRemoteIdleSession(nodeId: string, sessionId: string): void;
|
|
82
82
|
pruneExpiredRemoteIdleSessions(): void;
|
|
83
|
+
insertSessionDelivery(entry: {
|
|
84
|
+
id: string;
|
|
85
|
+
meshId: string;
|
|
86
|
+
nodeId?: string;
|
|
87
|
+
sessionId?: string;
|
|
88
|
+
providerType?: string;
|
|
89
|
+
taskId?: string;
|
|
90
|
+
kind: string;
|
|
91
|
+
priority?: number;
|
|
92
|
+
message: string;
|
|
93
|
+
status: string;
|
|
94
|
+
deliverAfter?: string;
|
|
95
|
+
expiresAt?: string;
|
|
96
|
+
sourceCoordinatorSessionId?: string;
|
|
97
|
+
sourceCoordinatorDaemonId?: string;
|
|
98
|
+
createdAt: string;
|
|
99
|
+
updatedAt: string;
|
|
100
|
+
}): void;
|
|
101
|
+
updateSessionDeliveryStatus(id: string, status: string, opts?: {
|
|
102
|
+
lastError?: string;
|
|
103
|
+
incrementAttempt?: boolean;
|
|
104
|
+
}): void;
|
|
105
|
+
getActiveSessionDeliveries(meshId: string, sessionId?: string): Array<{
|
|
106
|
+
id: string;
|
|
107
|
+
meshId: string;
|
|
108
|
+
nodeId: string | null;
|
|
109
|
+
sessionId: string | null;
|
|
110
|
+
providerType: string | null;
|
|
111
|
+
taskId: string | null;
|
|
112
|
+
kind: string;
|
|
113
|
+
priority: number;
|
|
114
|
+
message: string;
|
|
115
|
+
status: string;
|
|
116
|
+
deliverAfter: string | null;
|
|
117
|
+
expiresAt: string | null;
|
|
118
|
+
attemptCount: number;
|
|
119
|
+
sourceCoordinatorSessionId: string | null;
|
|
120
|
+
sourceCoordinatorDaemonId: string | null;
|
|
121
|
+
lastError: string | null;
|
|
122
|
+
createdAt: string;
|
|
123
|
+
updatedAt: string;
|
|
124
|
+
}>;
|
|
125
|
+
expireStaleSessionDeliveries(meshId: string): void;
|
|
126
|
+
deleteSessionDeliveries(meshId: string): void;
|
|
127
|
+
recordCompletionConflict(entry: {
|
|
128
|
+
id: string;
|
|
129
|
+
meshId: string;
|
|
130
|
+
fingerprint: string;
|
|
131
|
+
conflictingTaskId?: string;
|
|
132
|
+
conflictingSessionId?: string;
|
|
133
|
+
originalTaskId?: string;
|
|
134
|
+
originalSessionId?: string;
|
|
135
|
+
event: string;
|
|
136
|
+
createdAt: string;
|
|
137
|
+
}): void;
|
|
138
|
+
getRecentCompletionConflicts(meshId: string, limitMs?: number): Array<{
|
|
139
|
+
id: string;
|
|
140
|
+
meshId: string;
|
|
141
|
+
fingerprint: string;
|
|
142
|
+
conflictingTaskId: string | null;
|
|
143
|
+
conflictingSessionId: string | null;
|
|
144
|
+
originalTaskId: string | null;
|
|
145
|
+
originalSessionId: string | null;
|
|
146
|
+
event: string;
|
|
147
|
+
createdAt: string;
|
|
148
|
+
}>;
|
|
83
149
|
}
|
|
@@ -135,7 +135,15 @@ export declare class SpecDriver {
|
|
|
135
135
|
* explicit wake-up there's nothing to trigger the busy → idle
|
|
136
136
|
* downshift. */
|
|
137
137
|
private busyExpiryTimer;
|
|
138
|
+
/** Pending idle-commit timer. Armed when the evaluator first returns idle;
|
|
139
|
+
* fires after idle_hold_ms if no non-idle reading has cancelled it. */
|
|
140
|
+
private idleHoldTimer;
|
|
141
|
+
/** State snapshot captured when the idle hold was armed — emitted on commit. */
|
|
142
|
+
private pendingIdleState;
|
|
138
143
|
private specWatcher;
|
|
144
|
+
/** Ring buffer of committed state transitions (max 50). */
|
|
145
|
+
private stateHistory;
|
|
146
|
+
private prevStateAt;
|
|
139
147
|
constructor(opts: SpecDriverOpts);
|
|
140
148
|
/** Subscribe to outbound events. Returns an unsubscribe fn. */
|
|
141
149
|
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
@@ -147,6 +155,17 @@ export declare class SpecDriver {
|
|
|
147
155
|
col: number;
|
|
148
156
|
};
|
|
149
157
|
shutdown(): void;
|
|
158
|
+
private cancelIdleHold;
|
|
159
|
+
private pushHistory;
|
|
160
|
+
getStateHistory(): ReadonlyArray<{
|
|
161
|
+
stateId: string;
|
|
162
|
+
label: string;
|
|
163
|
+
at: number;
|
|
164
|
+
durationMs: number;
|
|
165
|
+
}>;
|
|
166
|
+
getLastBusyAt(): number;
|
|
167
|
+
hasIdleHoldPending(): boolean;
|
|
168
|
+
getSpecPath(): string;
|
|
150
169
|
private loadSpecOrThrow;
|
|
151
170
|
private buildAdapterOpts;
|
|
152
171
|
private armSpecWatcher;
|
|
@@ -249,6 +249,12 @@ export interface CliSpec {
|
|
|
249
249
|
* Absorbs per-frame flicker in TUIs that stream output through
|
|
250
250
|
* the same region as the spinner. */
|
|
251
251
|
busy_hold_ms?: number;
|
|
252
|
+
/** Min time the idle state must remain matched before it is
|
|
253
|
+
* committed. Filters transient idle flickers that appear during
|
|
254
|
+
* approval dismissals, layout reflows, or brief spinner gaps.
|
|
255
|
+
* Any non-idle reading within the window cancels the transition.
|
|
256
|
+
* When omitted the idle transition is immediate (legacy behaviour). */
|
|
257
|
+
idle_hold_ms?: number;
|
|
252
258
|
/** Min time after start() before a send_message is allowed to
|
|
253
259
|
* reach the PTY. Banner paints + auth flows + skill listings
|
|
254
260
|
* can keep the agent unable to accept input for several seconds
|
package/package.json
CHANGED
package/src/commands/router.ts
CHANGED
|
@@ -4178,6 +4178,25 @@ export class DaemonCommandRouter {
|
|
|
4178
4178
|
};
|
|
4179
4179
|
}
|
|
4180
4180
|
|
|
4181
|
+
case 'get_spec_debug': {
|
|
4182
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
|
|
4183
|
+
: typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
4184
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
4185
|
+
const target = this.deps.sessionRegistry.get(sessionId);
|
|
4186
|
+
if (!target) return { success: false, error: 'Session not found', sessionId };
|
|
4187
|
+
const adapter = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
|
|
4188
|
+
const snapshot = (adapter && typeof (adapter as any).getDebugSnapshot === 'function')
|
|
4189
|
+
? (adapter as any).getDebugSnapshot()
|
|
4190
|
+
: null;
|
|
4191
|
+
return {
|
|
4192
|
+
success: true,
|
|
4193
|
+
sessionId,
|
|
4194
|
+
providerType: target.providerType,
|
|
4195
|
+
isSpecProvider: snapshot !== null,
|
|
4196
|
+
snapshot,
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4181
4200
|
// ── User-level coordinator-prompt files (~/.adhdev/coordinator-prompts/).
|
|
4182
4201
|
// These live on this daemon's filesystem and never sync to the
|
|
4183
4202
|
// cloud / other daemons — they're per-machine config. The
|
package/src/index.ts
CHANGED
|
@@ -232,6 +232,10 @@ export { buildMeshHostRequiredFailure, createDefaultMeshHostMetadata, isMeshHost
|
|
|
232
232
|
export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, reconcileDirectDispatchCompletionFromTranscript } from './mesh/mesh-events.js';
|
|
233
233
|
export type { PendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
|
|
234
234
|
|
|
235
|
+
// ── Mesh Delivery Policy ──
|
|
236
|
+
export { resolveDeliveryDecision, createSessionDelivery, updateSessionDeliveryStatus, getActiveSessionDeliveries, recordCompletionConflict, getRecentCompletionConflicts } from './mesh/mesh-delivery-policy.js';
|
|
237
|
+
export type { MeshSessionDeliveryStatus, MeshSessionDeliveryKind, MeshDeliveryDecision, MeshDeliveryPolicyResult, SessionDeliveryRecord } from './mesh/mesh-delivery-policy.js';
|
|
238
|
+
|
|
235
239
|
// ── Mesh P2P Relay Failure Classification ──
|
|
236
240
|
export {
|
|
237
241
|
P2pRelayFailureError,
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Possible delivery statuses for a session delivery record.
|
|
6
|
+
*/
|
|
7
|
+
export type MeshSessionDeliveryStatus =
|
|
8
|
+
| 'queued'
|
|
9
|
+
| 'delivering'
|
|
10
|
+
| 'delivered'
|
|
11
|
+
| 'acked'
|
|
12
|
+
| 'completed'
|
|
13
|
+
| 'failed'
|
|
14
|
+
| 'expired'
|
|
15
|
+
| 'cancelled';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Kind of delivery — controls priority and policy handling.
|
|
19
|
+
*/
|
|
20
|
+
export type MeshSessionDeliveryKind =
|
|
21
|
+
| 'task'
|
|
22
|
+
| 'followup'
|
|
23
|
+
| 'approval'
|
|
24
|
+
| 'recovery'
|
|
25
|
+
| 'system_notice';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A session delivery decision — what to do when a task arrives for a session.
|
|
29
|
+
*/
|
|
30
|
+
export type MeshDeliveryDecision =
|
|
31
|
+
| 'immediate' // Session is idle: deliver now, create an 'acked' delivery record
|
|
32
|
+
| 'queued' // Session is busy: hold delivery until session becomes idle
|
|
33
|
+
| 'rejected'; // Session is terminal or unknown: cannot deliver
|
|
34
|
+
|
|
35
|
+
export interface MeshDeliveryPolicyResult {
|
|
36
|
+
decision: MeshDeliveryDecision;
|
|
37
|
+
reason: string;
|
|
38
|
+
/** When decision='queued', estimated deliver-after ISO timestamp if known. */
|
|
39
|
+
deliverAfter?: string;
|
|
40
|
+
/** Human-readable explanation for coordinator/operator. */
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Session statuses where immediate delivery is allowed.
|
|
46
|
+
* The session is ready to accept new work.
|
|
47
|
+
*/
|
|
48
|
+
const IMMEDIATE_DELIVERY_STATUSES = new Set([
|
|
49
|
+
'idle',
|
|
50
|
+
'waiting_input',
|
|
51
|
+
'ready',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Session statuses that indicate the session is busy but still alive.
|
|
56
|
+
* Delivery is queued rather than attempted immediately.
|
|
57
|
+
*/
|
|
58
|
+
const BUSY_DELIVERY_STATUSES = new Set([
|
|
59
|
+
'generating',
|
|
60
|
+
'running',
|
|
61
|
+
'streaming',
|
|
62
|
+
'busy',
|
|
63
|
+
'starting',
|
|
64
|
+
'initializing',
|
|
65
|
+
'waiting_approval',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Session statuses that indicate the session is permanently unavailable.
|
|
70
|
+
* Delivery should be rejected.
|
|
71
|
+
*/
|
|
72
|
+
const TERMINAL_DELIVERY_STATUSES = new Set([
|
|
73
|
+
'stopped',
|
|
74
|
+
'failed',
|
|
75
|
+
'terminated',
|
|
76
|
+
'exited',
|
|
77
|
+
'closed',
|
|
78
|
+
'deleted',
|
|
79
|
+
'error',
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Determine whether to deliver immediately, queue, or reject based on session status.
|
|
84
|
+
*
|
|
85
|
+
* This is a pure function — it does not write to any store.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveDeliveryDecision(
|
|
88
|
+
sessionStatus: string | undefined,
|
|
89
|
+
opts?: {
|
|
90
|
+
kind?: MeshSessionDeliveryKind;
|
|
91
|
+
/** When true, busy session immediate injection is allowed (provider-specific capability). */
|
|
92
|
+
allowBusyInjection?: boolean;
|
|
93
|
+
},
|
|
94
|
+
): MeshDeliveryPolicyResult {
|
|
95
|
+
const status = (sessionStatus || '').trim().toLowerCase();
|
|
96
|
+
|
|
97
|
+
if (!status) {
|
|
98
|
+
return {
|
|
99
|
+
decision: 'rejected',
|
|
100
|
+
reason: 'unknown_session_status',
|
|
101
|
+
message: 'Session status is unknown. Delivery rejected (fail-closed). Use mesh_launch_session to start a fresh session.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (IMMEDIATE_DELIVERY_STATUSES.has(status)) {
|
|
106
|
+
return {
|
|
107
|
+
decision: 'immediate',
|
|
108
|
+
reason: `session_${status}`,
|
|
109
|
+
message: `Session is ${status} — delivery allowed immediately.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (BUSY_DELIVERY_STATUSES.has(status)) {
|
|
114
|
+
if (opts?.allowBusyInjection) {
|
|
115
|
+
return {
|
|
116
|
+
decision: 'immediate',
|
|
117
|
+
reason: `session_${status}_busy_injection_allowed`,
|
|
118
|
+
message: `Session is ${status} but provider supports busy injection. Delivered immediately.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// approval-kind may be delivered to waiting_approval sessions
|
|
122
|
+
if (status === 'waiting_approval' && opts?.kind === 'approval') {
|
|
123
|
+
return {
|
|
124
|
+
decision: 'immediate',
|
|
125
|
+
reason: 'session_waiting_approval_approval_message',
|
|
126
|
+
message: 'Session is waiting for approval — approval message delivered immediately.',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
decision: 'queued',
|
|
131
|
+
reason: `session_${status}_busy`,
|
|
132
|
+
message: `Session is ${status}. Task queued for delivery when session becomes idle. Do not inject directly into a busy session.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (TERMINAL_DELIVERY_STATUSES.has(status)) {
|
|
137
|
+
return {
|
|
138
|
+
decision: 'rejected',
|
|
139
|
+
reason: `session_${status}_terminal`,
|
|
140
|
+
message: `Session is ${status} (terminal). Delivery rejected. Launch a new session before dispatching tasks.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Unknown/unrecognized status: fail-closed
|
|
145
|
+
return {
|
|
146
|
+
decision: 'rejected',
|
|
147
|
+
reason: 'unrecognized_session_status',
|
|
148
|
+
message: `Session status '${sessionStatus}' is not recognized. Delivery rejected (fail-closed). Inspect session state before retrying.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface SessionDeliveryRecord {
|
|
153
|
+
id: string;
|
|
154
|
+
meshId: string;
|
|
155
|
+
nodeId?: string;
|
|
156
|
+
sessionId?: string;
|
|
157
|
+
providerType?: string;
|
|
158
|
+
taskId?: string;
|
|
159
|
+
kind: MeshSessionDeliveryKind;
|
|
160
|
+
priority: number;
|
|
161
|
+
message: string;
|
|
162
|
+
status: MeshSessionDeliveryStatus;
|
|
163
|
+
deliverAfter?: string;
|
|
164
|
+
expiresAt?: string;
|
|
165
|
+
attemptCount: number;
|
|
166
|
+
sourceCoordinatorSessionId?: string;
|
|
167
|
+
sourceCoordinatorDaemonId?: string;
|
|
168
|
+
lastError?: string;
|
|
169
|
+
createdAt: string;
|
|
170
|
+
updatedAt: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create a delivery record in the store.
|
|
175
|
+
*/
|
|
176
|
+
export function createSessionDelivery(opts: {
|
|
177
|
+
meshId: string;
|
|
178
|
+
nodeId?: string;
|
|
179
|
+
sessionId?: string;
|
|
180
|
+
providerType?: string;
|
|
181
|
+
taskId?: string;
|
|
182
|
+
kind: MeshSessionDeliveryKind;
|
|
183
|
+
message: string;
|
|
184
|
+
status: MeshSessionDeliveryStatus;
|
|
185
|
+
priority?: number;
|
|
186
|
+
deliverAfter?: string;
|
|
187
|
+
expiresAt?: string;
|
|
188
|
+
sourceCoordinatorSessionId?: string;
|
|
189
|
+
sourceCoordinatorDaemonId?: string;
|
|
190
|
+
}): SessionDeliveryRecord {
|
|
191
|
+
const now = new Date().toISOString();
|
|
192
|
+
const id = randomUUID();
|
|
193
|
+
const record: SessionDeliveryRecord = {
|
|
194
|
+
id,
|
|
195
|
+
meshId: opts.meshId,
|
|
196
|
+
nodeId: opts.nodeId,
|
|
197
|
+
sessionId: opts.sessionId,
|
|
198
|
+
providerType: opts.providerType,
|
|
199
|
+
taskId: opts.taskId,
|
|
200
|
+
kind: opts.kind,
|
|
201
|
+
priority: opts.priority ?? 0,
|
|
202
|
+
message: opts.message,
|
|
203
|
+
status: opts.status,
|
|
204
|
+
deliverAfter: opts.deliverAfter,
|
|
205
|
+
expiresAt: opts.expiresAt,
|
|
206
|
+
attemptCount: 0,
|
|
207
|
+
sourceCoordinatorSessionId: opts.sourceCoordinatorSessionId,
|
|
208
|
+
sourceCoordinatorDaemonId: opts.sourceCoordinatorDaemonId,
|
|
209
|
+
createdAt: now,
|
|
210
|
+
updatedAt: now,
|
|
211
|
+
};
|
|
212
|
+
MeshRuntimeStore.getInstance().insertSessionDelivery({
|
|
213
|
+
id,
|
|
214
|
+
meshId: opts.meshId,
|
|
215
|
+
nodeId: opts.nodeId,
|
|
216
|
+
sessionId: opts.sessionId,
|
|
217
|
+
providerType: opts.providerType,
|
|
218
|
+
taskId: opts.taskId,
|
|
219
|
+
kind: opts.kind,
|
|
220
|
+
priority: opts.priority ?? 0,
|
|
221
|
+
message: opts.message,
|
|
222
|
+
status: opts.status,
|
|
223
|
+
deliverAfter: opts.deliverAfter,
|
|
224
|
+
expiresAt: opts.expiresAt,
|
|
225
|
+
sourceCoordinatorSessionId: opts.sourceCoordinatorSessionId,
|
|
226
|
+
sourceCoordinatorDaemonId: opts.sourceCoordinatorDaemonId,
|
|
227
|
+
createdAt: now,
|
|
228
|
+
updatedAt: now,
|
|
229
|
+
});
|
|
230
|
+
return record;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Update the status of a delivery record.
|
|
235
|
+
*/
|
|
236
|
+
export function updateSessionDeliveryStatus(
|
|
237
|
+
id: string,
|
|
238
|
+
status: MeshSessionDeliveryStatus,
|
|
239
|
+
opts?: { lastError?: string; incrementAttempt?: boolean },
|
|
240
|
+
): void {
|
|
241
|
+
try {
|
|
242
|
+
MeshRuntimeStore.getInstance().updateSessionDeliveryStatus(id, status, opts);
|
|
243
|
+
} catch { /* best-effort */ }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get active (non-terminal) deliveries for a mesh, optionally filtered by session.
|
|
248
|
+
*/
|
|
249
|
+
export function getActiveSessionDeliveries(meshId: string, sessionId?: string) {
|
|
250
|
+
try {
|
|
251
|
+
return MeshRuntimeStore.getInstance().getActiveSessionDeliveries(meshId, sessionId);
|
|
252
|
+
} catch {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Record a completion conflict diagnostic when a duplicate event points to
|
|
259
|
+
* different task/session than the already-seen event with the same fingerprint.
|
|
260
|
+
*/
|
|
261
|
+
export function recordCompletionConflict(opts: {
|
|
262
|
+
meshId: string;
|
|
263
|
+
fingerprint: string;
|
|
264
|
+
conflictingTaskId?: string;
|
|
265
|
+
conflictingSessionId?: string;
|
|
266
|
+
originalTaskId?: string;
|
|
267
|
+
originalSessionId?: string;
|
|
268
|
+
event: string;
|
|
269
|
+
}): void {
|
|
270
|
+
try {
|
|
271
|
+
MeshRuntimeStore.getInstance().recordCompletionConflict({
|
|
272
|
+
id: randomUUID(),
|
|
273
|
+
meshId: opts.meshId,
|
|
274
|
+
fingerprint: opts.fingerprint,
|
|
275
|
+
conflictingTaskId: opts.conflictingTaskId,
|
|
276
|
+
conflictingSessionId: opts.conflictingSessionId,
|
|
277
|
+
originalTaskId: opts.originalTaskId,
|
|
278
|
+
originalSessionId: opts.originalSessionId,
|
|
279
|
+
event: opts.event,
|
|
280
|
+
createdAt: new Date().toISOString(),
|
|
281
|
+
});
|
|
282
|
+
} catch { /* best-effort diagnostics */ }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get recent completion conflicts for diagnostic inspection.
|
|
287
|
+
*/
|
|
288
|
+
export function getRecentCompletionConflicts(meshId: string, limitMs?: number) {
|
|
289
|
+
try {
|
|
290
|
+
return MeshRuntimeStore.getInstance().getRecentCompletionConflicts(meshId, limitMs);
|
|
291
|
+
} catch {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function __clearSessionDeliveriesForTests(meshId: string): void {
|
|
297
|
+
MeshRuntimeStore.getInstance().deleteSessionDeliveries(meshId);
|
|
298
|
+
}
|