@cydm/happy-elves 0.1.0-beta.39 → 0.1.0-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -176,7 +176,13 @@ export function createExternalAgentRuntimeProvider(options) {
|
|
|
176
176
|
}));
|
|
177
177
|
}
|
|
178
178
|
function clientForAgent(agent) {
|
|
179
|
-
|
|
179
|
+
const client = clientsByAgent.get(agent);
|
|
180
|
+
if (!client)
|
|
181
|
+
return undefined;
|
|
182
|
+
if (client.isAvailable() && client.listAgents().some((profile) => profile.id === agent))
|
|
183
|
+
return client;
|
|
184
|
+
clientsByAgent.delete(agent);
|
|
185
|
+
return undefined;
|
|
180
186
|
}
|
|
181
187
|
function clientForHandle(handle) {
|
|
182
188
|
return handle.externalProviderId ? clients.find((client) => client.id === handle.externalProviderId) : undefined;
|
|
@@ -248,7 +254,7 @@ export function createExternalAgentRuntimeProvider(options) {
|
|
|
248
254
|
id: "fallback",
|
|
249
255
|
load: (cursor, pageLimit) => filterHistoricalSessionPage(fallbackHistoricalSessionPage({ ...input, cursor, limit: pageLimit }), input.cwdMode),
|
|
250
256
|
},
|
|
251
|
-
...clients.map((client) => ({
|
|
257
|
+
...clients.filter((client) => client.isAvailable()).map((client) => ({
|
|
252
258
|
id: `external:${client.id}`,
|
|
253
259
|
load: (cursor, pageLimit) => filterHistoricalSessionPage(client.request("listHistoricalSessions", { ...input, cursor, limit: pageLimit }).catch(emptyPageUnlessCursorStale), input.cwdMode),
|
|
254
260
|
})),
|
|
@@ -470,6 +476,9 @@ class ExternalProviderClient {
|
|
|
470
476
|
listAgents() {
|
|
471
477
|
return this.agents;
|
|
472
478
|
}
|
|
479
|
+
isAvailable() {
|
|
480
|
+
return this.ready && !!this.child && !this.child.killed && !this.exited;
|
|
481
|
+
}
|
|
473
482
|
diagnostics() {
|
|
474
483
|
const processAvailable = !!this.child && !this.child.killed && !this.exited;
|
|
475
484
|
return {
|
|
@@ -648,6 +657,7 @@ class ExternalProviderClient {
|
|
|
648
657
|
this.ready = false;
|
|
649
658
|
this.exited = true;
|
|
650
659
|
this.lastError = error.message;
|
|
660
|
+
this.agents = [];
|
|
651
661
|
for (const [id, pending] of this.pending) {
|
|
652
662
|
clearTimeout(pending.timer);
|
|
653
663
|
pending.reject(error);
|
package/apps/daemon/package.json
CHANGED
package/apps/relay/dist/db.js
CHANGED
|
@@ -116,6 +116,26 @@ export function initDb(db) {
|
|
|
116
116
|
FOREIGN KEY (machine_id) REFERENCES machines(id) ON DELETE CASCADE
|
|
117
117
|
);
|
|
118
118
|
|
|
119
|
+
CREATE TABLE IF NOT EXISTS pending_session_head_advances (
|
|
120
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
121
|
+
account_id TEXT NOT NULL,
|
|
122
|
+
session_id TEXT NOT NULL,
|
|
123
|
+
machine_id TEXT NOT NULL,
|
|
124
|
+
message_id TEXT,
|
|
125
|
+
current_head TEXT NOT NULL,
|
|
126
|
+
last_turn_id TEXT NOT NULL,
|
|
127
|
+
basis_turn_id TEXT NOT NULL,
|
|
128
|
+
basis_event_message_id TEXT NOT NULL,
|
|
129
|
+
basis_turn_seq INTEGER NOT NULL,
|
|
130
|
+
basis_occurred_at INTEGER,
|
|
131
|
+
encrypted_metadata TEXT,
|
|
132
|
+
created_at INTEGER NOT NULL,
|
|
133
|
+
updated_at INTEGER NOT NULL,
|
|
134
|
+
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
|
135
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
|
|
136
|
+
FOREIGN KEY (machine_id) REFERENCES machines(id) ON DELETE CASCADE
|
|
137
|
+
);
|
|
138
|
+
|
|
119
139
|
CREATE TABLE IF NOT EXISTS command_results (
|
|
120
140
|
account_id TEXT NOT NULL,
|
|
121
141
|
request_id TEXT NOT NULL,
|
|
@@ -131,6 +151,8 @@ export function initDb(db) {
|
|
|
131
151
|
CREATE INDEX IF NOT EXISTS idx_events_session ON session_events(session_id, id);
|
|
132
152
|
CREATE INDEX IF NOT EXISTS idx_turn_aliases_runtime
|
|
133
153
|
ON session_turn_aliases(account_id, session_id, runtime_turn_id);
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_pending_head_session
|
|
155
|
+
ON pending_session_head_advances(account_id, session_id, machine_id);
|
|
134
156
|
CREATE INDEX IF NOT EXISTS idx_command_results_created ON command_results(created_at);
|
|
135
157
|
CREATE INDEX IF NOT EXISTS idx_controller_invites_account ON controller_invites(account_id, created_at DESC);
|
|
136
158
|
`);
|
|
@@ -157,6 +179,9 @@ export function initDb(db) {
|
|
|
157
179
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_events_machine_message
|
|
158
180
|
ON session_events(account_id, machine_id, message_id)
|
|
159
181
|
WHERE message_id IS NOT NULL;
|
|
182
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_head_machine_message
|
|
183
|
+
ON pending_session_head_advances(account_id, machine_id, message_id)
|
|
184
|
+
WHERE message_id IS NOT NULL;
|
|
160
185
|
`);
|
|
161
186
|
repairHistoricalEventTimestamps(db);
|
|
162
187
|
}
|
|
@@ -142,6 +142,7 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
142
142
|
acknowledgeMachineMessage();
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
|
+
clearPendingSessionHeadAdvances(db, connection.accountId, message.sessionId, connection.machineId);
|
|
145
146
|
const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
|
|
146
147
|
if (row)
|
|
147
148
|
broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
|
|
@@ -272,6 +273,7 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
272
273
|
WHERE account_id = ? AND session_id = ? AND machine_id = ?`).run(connection.accountId, message.sessionId, connection.machineId);
|
|
273
274
|
db.prepare(`DELETE FROM session_turn_aliases
|
|
274
275
|
WHERE account_id = ? AND session_id = ? AND machine_id = ?`).run(connection.accountId, message.sessionId, connection.machineId);
|
|
276
|
+
clearPendingSessionHeadAdvances(db, connection.accountId, message.sessionId, connection.machineId);
|
|
275
277
|
for (const event of message.events) {
|
|
276
278
|
const result = db
|
|
277
279
|
.prepare(`INSERT INTO session_events
|
|
@@ -323,7 +325,10 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
323
325
|
db.exec("ROLLBACK");
|
|
324
326
|
throw error;
|
|
325
327
|
}
|
|
326
|
-
const
|
|
328
|
+
const pendingSessionRow = inserted.length > 0
|
|
329
|
+
? applyPendingSessionHeadAdvances(db, connection.accountId, message.sessionId, connection.machineId, now())
|
|
330
|
+
: undefined;
|
|
331
|
+
const row = pendingSessionRow ?? db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
|
|
327
332
|
if (row) {
|
|
328
333
|
const session = sessionSnapshot(row);
|
|
329
334
|
broadcastControllers(connection.accountId, {
|
|
@@ -387,7 +392,8 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
387
392
|
db.exec("ROLLBACK");
|
|
388
393
|
throw error;
|
|
389
394
|
}
|
|
390
|
-
const
|
|
395
|
+
const pendingSessionRow = applyPendingSessionHeadAdvances(db, connection.accountId, message.sessionId, connection.machineId, now());
|
|
396
|
+
const row = pendingSessionRow ?? db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
|
|
391
397
|
if (row) {
|
|
392
398
|
const session = sessionSnapshot(row);
|
|
393
399
|
broadcastControllers(connection.accountId, {
|
|
@@ -403,10 +409,19 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
403
409
|
return;
|
|
404
410
|
}
|
|
405
411
|
if (message.type === "machine:sessionHeadAdvanced") {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
412
|
+
const ts = now();
|
|
413
|
+
const input = {
|
|
414
|
+
accountId: connection.accountId,
|
|
415
|
+
sessionId: message.sessionId,
|
|
416
|
+
machineId: connection.machineId,
|
|
417
|
+
messageId,
|
|
418
|
+
currentHead: message.currentHead,
|
|
419
|
+
lastTurnId: message.lastTurnId,
|
|
420
|
+
basis: message.basis,
|
|
421
|
+
...(message.encryptedMetadata ? { encryptedMetadata: message.encryptedMetadata } : {}),
|
|
422
|
+
};
|
|
423
|
+
const result = applySessionHeadAdvance(db, input, ts);
|
|
424
|
+
if (result.status === "session-not-found") {
|
|
410
425
|
broadcastError(connection.accountId, {
|
|
411
426
|
type: "server:error",
|
|
412
427
|
code: "SESSION_NOT_FOUND",
|
|
@@ -415,16 +430,17 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
415
430
|
acknowledgeMachineMessage();
|
|
416
431
|
return;
|
|
417
432
|
}
|
|
418
|
-
|
|
419
|
-
|
|
433
|
+
if (result.status === "missing") {
|
|
434
|
+
storePendingSessionHeadAdvance(db, input, ts);
|
|
420
435
|
broadcastError(connection.accountId, {
|
|
421
436
|
type: "server:error",
|
|
422
437
|
code: "SESSION_HEAD_BASIS_MISSING",
|
|
423
|
-
message: "Head advance basis event is not available yet",
|
|
438
|
+
message: "Head advance basis event is not available yet; update will be applied when the event arrives",
|
|
424
439
|
});
|
|
440
|
+
acknowledgeMachineMessage();
|
|
425
441
|
return;
|
|
426
442
|
}
|
|
427
|
-
if (
|
|
443
|
+
if (result.status === "mismatch") {
|
|
428
444
|
broadcastError(connection.accountId, {
|
|
429
445
|
type: "server:error",
|
|
430
446
|
code: "SESSION_HEAD_BASIS_MISMATCH",
|
|
@@ -433,25 +449,7 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
433
449
|
acknowledgeMachineMessage();
|
|
434
450
|
return;
|
|
435
451
|
}
|
|
436
|
-
const
|
|
437
|
-
const currentOrder = sessionHeadBasisOrder(db, sessionRow);
|
|
438
|
-
const rewriteFenced = typeof sessionRow.head_basis_message_id === "string" &&
|
|
439
|
-
sessionRow.head_basis_message_id.startsWith(REWRITE_HEAD_BASIS_PREFIX);
|
|
440
|
-
const shouldAdvance = !rewriteFenced && compareHeadBasisOrder(nextOrder, currentOrder) > 0;
|
|
441
|
-
const ts = now();
|
|
442
|
-
if (shouldAdvance) {
|
|
443
|
-
db.prepare(`UPDATE sessions
|
|
444
|
-
SET current_head = ?,
|
|
445
|
-
last_turn_id = ?,
|
|
446
|
-
head_basis_event_id = ?,
|
|
447
|
-
head_basis_message_id = ?,
|
|
448
|
-
head_basis_logical_time = ?,
|
|
449
|
-
head_basis_turn_seq = ?,
|
|
450
|
-
encrypted_metadata = COALESCE(?, encrypted_metadata),
|
|
451
|
-
updated_at = ?
|
|
452
|
-
WHERE id = ? AND account_id = ? AND machine_id = ?`).run(message.currentHead, message.lastTurnId, basisEvent.id, basisEvent.message_id, nextOrder.time, nextOrder.seq, message.encryptedMetadata ? JSON.stringify(message.encryptedMetadata) : null, ts, message.sessionId, connection.accountId, connection.machineId);
|
|
453
|
-
}
|
|
454
|
-
const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
|
|
452
|
+
const row = result.session ?? db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
|
|
455
453
|
if (row)
|
|
456
454
|
broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
|
|
457
455
|
acknowledgeMachineMessage();
|
|
@@ -483,7 +481,10 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
483
481
|
occurredAt: message.occurredAt,
|
|
484
482
|
payload: message.payload,
|
|
485
483
|
});
|
|
484
|
+
const pendingSessionRow = applyPendingSessionHeadAdvances(db, connection.accountId, message.sessionId, connection.machineId, now());
|
|
486
485
|
broadcastControllers(connection.accountId, { type: "server:event", event });
|
|
486
|
+
if (pendingSessionRow)
|
|
487
|
+
broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(pendingSessionRow) });
|
|
487
488
|
acknowledgeMachineMessage();
|
|
488
489
|
return;
|
|
489
490
|
}
|
|
@@ -582,6 +583,94 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
582
583
|
function machineMessageId(message) {
|
|
583
584
|
return "messageId" in message && typeof message.messageId === "string" ? message.messageId : undefined;
|
|
584
585
|
}
|
|
586
|
+
function storePendingSessionHeadAdvance(db, input, ts) {
|
|
587
|
+
db.prepare(`INSERT INTO pending_session_head_advances
|
|
588
|
+
(account_id, session_id, machine_id, message_id, current_head, last_turn_id,
|
|
589
|
+
basis_turn_id, basis_event_message_id, basis_turn_seq, basis_occurred_at,
|
|
590
|
+
encrypted_metadata, created_at, updated_at)
|
|
591
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
592
|
+
ON CONFLICT(account_id, machine_id, message_id) WHERE message_id IS NOT NULL
|
|
593
|
+
DO UPDATE SET
|
|
594
|
+
current_head = excluded.current_head,
|
|
595
|
+
last_turn_id = excluded.last_turn_id,
|
|
596
|
+
basis_turn_id = excluded.basis_turn_id,
|
|
597
|
+
basis_event_message_id = excluded.basis_event_message_id,
|
|
598
|
+
basis_turn_seq = excluded.basis_turn_seq,
|
|
599
|
+
basis_occurred_at = excluded.basis_occurred_at,
|
|
600
|
+
encrypted_metadata = excluded.encrypted_metadata,
|
|
601
|
+
updated_at = excluded.updated_at`).run(input.accountId, input.sessionId, input.machineId, input.messageId ?? null, input.currentHead, input.lastTurnId, input.basis.turnId, input.basis.eventMessageId, input.basis.turnSeq, input.basis.occurredAt ?? null, input.encryptedMetadata ? JSON.stringify(input.encryptedMetadata) : null, ts, ts);
|
|
602
|
+
}
|
|
603
|
+
function clearPendingSessionHeadAdvances(db, accountId, sessionId, machineId) {
|
|
604
|
+
db.prepare(`DELETE FROM pending_session_head_advances
|
|
605
|
+
WHERE account_id = ? AND session_id = ? AND machine_id = ?`).run(accountId, sessionId, machineId);
|
|
606
|
+
}
|
|
607
|
+
function applyPendingSessionHeadAdvances(db, accountId, sessionId, machineId, ts) {
|
|
608
|
+
const pending = db
|
|
609
|
+
.prepare(`SELECT * FROM pending_session_head_advances
|
|
610
|
+
WHERE account_id = ? AND session_id = ? AND machine_id = ?
|
|
611
|
+
ORDER BY created_at, id`)
|
|
612
|
+
.all(accountId, sessionId, machineId);
|
|
613
|
+
let latestSession;
|
|
614
|
+
for (const row of pending) {
|
|
615
|
+
const result = applySessionHeadAdvance(db, pendingRowToHeadAdvanceInput(row), ts);
|
|
616
|
+
if (result.status === "missing")
|
|
617
|
+
continue;
|
|
618
|
+
db.prepare("DELETE FROM pending_session_head_advances WHERE id = ?").run(row.id);
|
|
619
|
+
latestSession = result.session ?? latestSession;
|
|
620
|
+
}
|
|
621
|
+
return latestSession;
|
|
622
|
+
}
|
|
623
|
+
function pendingRowToHeadAdvanceInput(row) {
|
|
624
|
+
return {
|
|
625
|
+
accountId: row.account_id,
|
|
626
|
+
sessionId: row.session_id,
|
|
627
|
+
machineId: row.machine_id,
|
|
628
|
+
messageId: row.message_id ?? undefined,
|
|
629
|
+
currentHead: row.current_head,
|
|
630
|
+
lastTurnId: row.last_turn_id,
|
|
631
|
+
basis: {
|
|
632
|
+
turnId: row.basis_turn_id,
|
|
633
|
+
eventMessageId: row.basis_event_message_id,
|
|
634
|
+
turnSeq: row.basis_turn_seq,
|
|
635
|
+
...(row.basis_occurred_at !== null ? { occurredAt: row.basis_occurred_at } : {}),
|
|
636
|
+
},
|
|
637
|
+
...(row.encrypted_metadata ? { encryptedMetadata: JSON.parse(row.encrypted_metadata) } : {}),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function applySessionHeadAdvance(db, input, ts) {
|
|
641
|
+
const sessionRow = db
|
|
642
|
+
.prepare("SELECT * FROM sessions WHERE id = ? AND account_id = ? AND machine_id = ?")
|
|
643
|
+
.get(input.sessionId, input.accountId, input.machineId);
|
|
644
|
+
if (!sessionRow)
|
|
645
|
+
return { status: "session-not-found" };
|
|
646
|
+
const basisEvent = resolveHeadAdvanceBasisEvent(db, input.accountId, input.sessionId, input.machineId, input.basis);
|
|
647
|
+
if (!basisEvent)
|
|
648
|
+
return { status: "missing", session: sessionRow };
|
|
649
|
+
if (basisEvent.turn_id !== input.basis.turnId || basisEvent.turn_seq !== input.basis.turnSeq) {
|
|
650
|
+
return { status: "mismatch", session: sessionRow };
|
|
651
|
+
}
|
|
652
|
+
const nextOrder = logicalEventOrder(basisEvent);
|
|
653
|
+
const currentOrder = sessionHeadBasisOrder(db, sessionRow);
|
|
654
|
+
const rewriteFenced = typeof sessionRow.head_basis_message_id === "string" &&
|
|
655
|
+
sessionRow.head_basis_message_id.startsWith(REWRITE_HEAD_BASIS_PREFIX);
|
|
656
|
+
const comparison = compareHeadBasisOrder(nextOrder, currentOrder);
|
|
657
|
+
const currentHeadIsBasisEvent = sessionRow.current_head === `event:${basisEvent.id}`;
|
|
658
|
+
const shouldAdvance = !rewriteFenced && (comparison > 0 || (comparison === 0 && currentHeadIsBasisEvent));
|
|
659
|
+
if (!shouldAdvance)
|
|
660
|
+
return { status: "noop", session: sessionRow };
|
|
661
|
+
db.prepare(`UPDATE sessions
|
|
662
|
+
SET current_head = ?,
|
|
663
|
+
last_turn_id = ?,
|
|
664
|
+
head_basis_event_id = ?,
|
|
665
|
+
head_basis_message_id = ?,
|
|
666
|
+
head_basis_logical_time = ?,
|
|
667
|
+
head_basis_turn_seq = ?,
|
|
668
|
+
encrypted_metadata = COALESCE(?, encrypted_metadata),
|
|
669
|
+
updated_at = ?
|
|
670
|
+
WHERE id = ? AND account_id = ? AND machine_id = ?`).run(input.currentHead, input.lastTurnId, basisEvent.id, basisEvent.message_id, nextOrder.time, nextOrder.seq, input.encryptedMetadata ? JSON.stringify(input.encryptedMetadata) : null, ts, input.sessionId, input.accountId, input.machineId);
|
|
671
|
+
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(input.sessionId);
|
|
672
|
+
return { status: "applied", session };
|
|
673
|
+
}
|
|
585
674
|
function compareHeadBasisOrder(nextOrder, currentOrder) {
|
|
586
675
|
if (!currentOrder)
|
|
587
676
|
return 1;
|
|
@@ -63,6 +63,22 @@ export type EventRow = {
|
|
|
63
63
|
created_at: number;
|
|
64
64
|
occurred_at: number | null;
|
|
65
65
|
};
|
|
66
|
+
export type PendingSessionHeadAdvanceRow = {
|
|
67
|
+
id: number;
|
|
68
|
+
account_id: string;
|
|
69
|
+
session_id: string;
|
|
70
|
+
machine_id: string;
|
|
71
|
+
message_id: string | null;
|
|
72
|
+
current_head: string;
|
|
73
|
+
last_turn_id: string;
|
|
74
|
+
basis_turn_id: string;
|
|
75
|
+
basis_event_message_id: string;
|
|
76
|
+
basis_turn_seq: number;
|
|
77
|
+
basis_occurred_at: number | null;
|
|
78
|
+
encrypted_metadata: string | null;
|
|
79
|
+
created_at: number;
|
|
80
|
+
updated_at: number;
|
|
81
|
+
};
|
|
66
82
|
export type CommandResultRow = {
|
|
67
83
|
account_id: string;
|
|
68
84
|
request_id: string;
|