happy-imou-cloud 2.0.18 → 2.0.19
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/bin/happy-cloud.mjs +1 -1
- package/dist/{BaseReasoningProcessor-DrAN-2bw.cjs → BaseReasoningProcessor-CTaamD5D.cjs} +5 -3
- package/dist/{BaseReasoningProcessor-DajL_jtA.mjs → BaseReasoningProcessor-pY3tfQ0E.mjs} +5 -3
- package/dist/{ProviderSelectionHandler-DiHuEbf_.mjs → ProviderSelectionHandler-C8pLBbE4.mjs} +2 -2
- package/dist/{ProviderSelectionHandler-BOAcP_CE.cjs → ProviderSelectionHandler-CYs2k-z1.cjs} +2 -2
- package/dist/{api-DLI3Zloa.mjs → api-B922KGF8.mjs} +748 -41
- package/dist/{api-eDp10nsY.cjs → api-DmpNsXS1.cjs} +748 -41
- package/dist/{command-BJu-MaCp.cjs → command-B4L9Deq_.cjs} +4 -4
- package/dist/{command-BYxsn0_c.mjs → command-uX614XS4.mjs} +4 -4
- package/dist/{index-CjWtQyJu.mjs → index-BkAY8k_1.mjs} +203 -17
- package/dist/{index-bzGCZJLB.cjs → index-D0VIxWJC.cjs} +206 -20
- package/dist/index.cjs +3 -3
- package/dist/index.mjs +3 -3
- package/dist/lib.cjs +2 -2
- package/dist/lib.d.cts +1054 -0
- package/dist/lib.d.mts +1054 -0
- package/dist/lib.mjs +2 -2
- package/dist/{persistence-DicQpgl6.mjs → persistence-BfGHkCeJ.mjs} +1 -1
- package/dist/{persistence-CSaoFYvt.cjs → persistence-ZZDZ6dl9.cjs} +1 -1
- package/dist/{registerKillSessionHandler-BWCjaf5S.mjs → registerKillSessionHandler-BqeqMcNV.mjs} +3 -3
- package/dist/{registerKillSessionHandler-BeMsOt9D.cjs → registerKillSessionHandler-DZ1sXUOg.cjs} +3 -3
- package/dist/{runClaude-BaDVGV4k.mjs → runClaude-CVBty_g7.mjs} +5 -5
- package/dist/{runClaude-CERpGT0L.cjs → runClaude-taQoQffg.cjs} +5 -5
- package/dist/{runCodex-Z12g3g5P.cjs → runCodex-JcA8vuC7.cjs} +111 -15
- package/dist/{runCodex-Bik9wE7F.mjs → runCodex-jA1hK-nj.mjs} +111 -15
- package/dist/{runGemini-Bh59hP_q.mjs → runGemini-BqKLrnid.mjs} +6 -6
- package/dist/{runGemini-Chas-o_m.cjs → runGemini-DZ0x4eaa.cjs} +6 -6
- package/package.json +1 -1
- package/scripts/build.mjs +66 -66
- package/scripts/devtools/README.md +9 -9
- package/scripts/e2e/fake-codex-acp-agent.mjs +139 -139
- package/scripts/e2e/local-server-session-roundtrip.mjs +1063 -1063
- package/scripts/release-smoke.mjs +8 -0
|
@@ -4,9 +4,9 @@ import { appendFileSync } from 'fs';
|
|
|
4
4
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { join, basename } from 'node:path';
|
|
7
|
+
import { z } from 'zod';
|
|
7
8
|
import { EventEmitter } from 'node:events';
|
|
8
9
|
import { io } from 'socket.io-client';
|
|
9
|
-
import { z } from 'zod';
|
|
10
10
|
import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash as createHash$1, createHmac, hkdfSync } from 'node:crypto';
|
|
11
11
|
import tweetnacl from 'tweetnacl';
|
|
12
12
|
import { readFile, stat, writeFile, readdir } from 'fs/promises';
|
|
@@ -16,7 +16,7 @@ import { spawn } from 'node:child_process';
|
|
|
16
16
|
import { Expo } from 'expo-server-sdk';
|
|
17
17
|
|
|
18
18
|
var name = "happy-imou-cloud";
|
|
19
|
-
var version = "2.0.
|
|
19
|
+
var version = "2.0.19";
|
|
20
20
|
var description = "hicloud - Imou 企业定制版。关键是 happy!移动端远程 AI 编程工具,支持 Claude Code、Codex 和 Gemini CLI";
|
|
21
21
|
var author = "long.zhu";
|
|
22
22
|
var license = "MIT";
|
|
@@ -430,7 +430,7 @@ async function listDaemonLogFiles(limit = 50) {
|
|
|
430
430
|
return { file, path: fullPath, modified: stats.mtime };
|
|
431
431
|
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
432
432
|
try {
|
|
433
|
-
const { readDaemonState } = await import('./persistence-
|
|
433
|
+
const { readDaemonState } = await import('./persistence-BfGHkCeJ.mjs');
|
|
434
434
|
const state = await readDaemonState();
|
|
435
435
|
if (!state) {
|
|
436
436
|
return logs;
|
|
@@ -564,16 +564,54 @@ function decrypt(key, variant, data) {
|
|
|
564
564
|
}
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
+
const SessionRuntimeIndexSchema = z.object({
|
|
568
|
+
machineId: z.string().nullish(),
|
|
569
|
+
hostPid: z.number().int().nullish(),
|
|
570
|
+
startedBy: z.string().nullish(),
|
|
571
|
+
lifecycleState: z.string().nullish(),
|
|
572
|
+
flavor: z.string().nullish()
|
|
573
|
+
});
|
|
574
|
+
const ProtocolV3DescriptorSchema = z.object({
|
|
575
|
+
protocolVersion: z.string(),
|
|
576
|
+
serverVersion: z.string().optional(),
|
|
577
|
+
capabilities: z.array(z.string()),
|
|
578
|
+
legacyFallback: z.object({
|
|
579
|
+
http: z.array(z.string()),
|
|
580
|
+
websocketPath: z.string()
|
|
581
|
+
}).optional()
|
|
582
|
+
});
|
|
583
|
+
const ProtocolV3CapabilitiesResponseSchema = z.object({
|
|
584
|
+
protocol: ProtocolV3DescriptorSchema
|
|
585
|
+
});
|
|
567
586
|
const SessionMessageContentSchema = z.object({
|
|
568
587
|
c: z.string(),
|
|
569
588
|
// Base64 encoded encrypted content
|
|
570
589
|
t: z.literal("encrypted")
|
|
571
590
|
});
|
|
591
|
+
const NewSessionBodySchema = z.object({
|
|
592
|
+
t: z.literal("new-session"),
|
|
593
|
+
id: z.string(),
|
|
594
|
+
seq: z.number(),
|
|
595
|
+
title: z.string().nullable().optional(),
|
|
596
|
+
metadata: z.string(),
|
|
597
|
+
metadataVersion: z.number(),
|
|
598
|
+
agentState: z.string().nullable(),
|
|
599
|
+
agentStateVersion: z.number(),
|
|
600
|
+
dataEncryptionKey: z.string().nullable().optional(),
|
|
601
|
+
active: z.boolean().optional(),
|
|
602
|
+
activeAt: z.number().optional(),
|
|
603
|
+
createdAt: z.number().optional(),
|
|
604
|
+
updatedAt: z.number().optional(),
|
|
605
|
+
sessionIndex: SessionRuntimeIndexSchema.nullish()
|
|
606
|
+
});
|
|
572
607
|
const UpdateBodySchema = z.object({
|
|
573
608
|
message: z.object({
|
|
574
609
|
id: z.string(),
|
|
575
610
|
seq: z.number(),
|
|
576
|
-
content: SessionMessageContentSchema
|
|
611
|
+
content: SessionMessageContentSchema,
|
|
612
|
+
localId: z.string().nullable().optional(),
|
|
613
|
+
createdAt: z.number().optional(),
|
|
614
|
+
updatedAt: z.number().optional()
|
|
577
615
|
}),
|
|
578
616
|
sid: z.string(),
|
|
579
617
|
// Session ID
|
|
@@ -581,16 +619,25 @@ const UpdateBodySchema = z.object({
|
|
|
581
619
|
});
|
|
582
620
|
const UpdateSessionBodySchema = z.object({
|
|
583
621
|
t: z.literal("update-session"),
|
|
584
|
-
sid: z.string(),
|
|
622
|
+
sid: z.string().optional(),
|
|
623
|
+
id: z.string().optional(),
|
|
624
|
+
changeSeq: z.number().optional(),
|
|
625
|
+
title: z.string().nullable().optional(),
|
|
585
626
|
metadata: z.object({
|
|
586
627
|
version: z.number(),
|
|
587
628
|
value: z.string()
|
|
588
629
|
}).nullish(),
|
|
630
|
+
sessionIndex: SessionRuntimeIndexSchema.nullish(),
|
|
589
631
|
agentState: z.object({
|
|
590
632
|
version: z.number(),
|
|
591
633
|
value: z.string()
|
|
592
634
|
}).nullish()
|
|
593
635
|
});
|
|
636
|
+
const DeleteSessionBodySchema = z.object({
|
|
637
|
+
t: z.literal("delete-session"),
|
|
638
|
+
sid: z.string(),
|
|
639
|
+
changeSeq: z.number().optional()
|
|
640
|
+
});
|
|
594
641
|
const UpdateMachineBodySchema = z.object({
|
|
595
642
|
t: z.literal("update-machine"),
|
|
596
643
|
machineId: z.string(),
|
|
@@ -607,9 +654,11 @@ z.object({
|
|
|
607
654
|
id: z.string(),
|
|
608
655
|
seq: z.number(),
|
|
609
656
|
body: z.union([
|
|
657
|
+
NewSessionBodySchema,
|
|
610
658
|
UpdateBodySchema,
|
|
611
659
|
UpdateSessionBodySchema,
|
|
612
|
-
UpdateMachineBodySchema
|
|
660
|
+
UpdateMachineBodySchema,
|
|
661
|
+
DeleteSessionBodySchema
|
|
613
662
|
]),
|
|
614
663
|
createdAt: z.number()
|
|
615
664
|
});
|
|
@@ -644,6 +693,87 @@ z.object({
|
|
|
644
693
|
seq: z.number(),
|
|
645
694
|
updatedAt: z.number()
|
|
646
695
|
});
|
|
696
|
+
const ProtocolV3SessionSchema = z.object({
|
|
697
|
+
id: z.string(),
|
|
698
|
+
lastChangeSeq: z.number(),
|
|
699
|
+
title: z.string().nullable().optional(),
|
|
700
|
+
metadata: z.string().optional(),
|
|
701
|
+
metadataVersion: z.number().optional(),
|
|
702
|
+
agentState: z.string().nullable().optional(),
|
|
703
|
+
agentStateVersion: z.number().optional(),
|
|
704
|
+
dataEncryptionKey: z.string().nullable().optional(),
|
|
705
|
+
active: z.boolean().optional(),
|
|
706
|
+
activeAt: z.number().optional(),
|
|
707
|
+
pendingCount: z.number().optional(),
|
|
708
|
+
pendingVersion: z.number().optional(),
|
|
709
|
+
createdAt: z.number().optional(),
|
|
710
|
+
updatedAt: z.number().optional(),
|
|
711
|
+
deleted: z.boolean().optional()
|
|
712
|
+
});
|
|
713
|
+
const ProtocolV3SessionMessageSchema = z.object({
|
|
714
|
+
id: z.string(),
|
|
715
|
+
seq: z.number(),
|
|
716
|
+
localId: z.string().nullable(),
|
|
717
|
+
content: SessionMessageContentSchema,
|
|
718
|
+
createdAt: z.number(),
|
|
719
|
+
updatedAt: z.number()
|
|
720
|
+
});
|
|
721
|
+
const ProtocolV3SessionSnapshotSchema = z.object({
|
|
722
|
+
messages: z.array(ProtocolV3SessionMessageSchema),
|
|
723
|
+
truncated: z.boolean(),
|
|
724
|
+
oldestRetainedSeq: z.number().nullable()
|
|
725
|
+
});
|
|
726
|
+
const ProtocolV3SessionSnapshotResponseSchema = z.object({
|
|
727
|
+
protocol: ProtocolV3DescriptorSchema,
|
|
728
|
+
session: ProtocolV3SessionSchema,
|
|
729
|
+
snapshot: ProtocolV3SessionSnapshotSchema
|
|
730
|
+
});
|
|
731
|
+
const ProtocolV3SessionChangeSchema = z.object({
|
|
732
|
+
changeSeq: z.number(),
|
|
733
|
+
changeType: z.string(),
|
|
734
|
+
createdAt: z.number(),
|
|
735
|
+
payload: z.record(z.string(), z.unknown())
|
|
736
|
+
});
|
|
737
|
+
z.object({
|
|
738
|
+
protocol: ProtocolV3DescriptorSchema,
|
|
739
|
+
connectionType: z.enum(["session-scoped", "user-scoped", "machine-scoped"])
|
|
740
|
+
});
|
|
741
|
+
z.object({
|
|
742
|
+
sessionId: z.string(),
|
|
743
|
+
change: ProtocolV3SessionChangeSchema
|
|
744
|
+
});
|
|
745
|
+
const SessionListItemSchema = z.object({
|
|
746
|
+
id: z.string(),
|
|
747
|
+
seq: z.number(),
|
|
748
|
+
title: z.string().nullable().optional(),
|
|
749
|
+
createdAt: z.number(),
|
|
750
|
+
updatedAt: z.number(),
|
|
751
|
+
active: z.boolean(),
|
|
752
|
+
activeAt: z.number(),
|
|
753
|
+
metadata: z.string(),
|
|
754
|
+
metadataVersion: z.number(),
|
|
755
|
+
agentState: z.string().nullable(),
|
|
756
|
+
agentStateVersion: z.number(),
|
|
757
|
+
dataEncryptionKey: z.string().nullable().optional(),
|
|
758
|
+
pendingCount: z.number().optional(),
|
|
759
|
+
pendingVersion: z.number().optional(),
|
|
760
|
+
lastMessage: z.unknown().nullable().optional(),
|
|
761
|
+
sessionIndex: SessionRuntimeIndexSchema.nullish()
|
|
762
|
+
});
|
|
763
|
+
const SessionListResponseSchema = z.object({
|
|
764
|
+
sessions: z.array(SessionListItemSchema)
|
|
765
|
+
});
|
|
766
|
+
const ProtocolV3SessionChangesCursorSchema = z.object({
|
|
767
|
+
afterSeq: z.number(),
|
|
768
|
+
lastChangeSeq: z.number(),
|
|
769
|
+
hasMore: z.boolean()
|
|
770
|
+
});
|
|
771
|
+
const ProtocolV3SessionChangesResponseSchema = z.object({
|
|
772
|
+
protocol: ProtocolV3DescriptorSchema,
|
|
773
|
+
session: ProtocolV3SessionSchema,
|
|
774
|
+
cursor: ProtocolV3SessionChangesCursorSchema,
|
|
775
|
+
changes: z.array(ProtocolV3SessionChangeSchema)
|
|
776
|
+
});
|
|
647
777
|
const MessageMetaSchema = z.object({
|
|
648
778
|
sentFrom: z.string().optional(),
|
|
649
779
|
// Source identifier
|
|
@@ -695,6 +825,27 @@ const AgentMessageSchema = z.object({
|
|
|
695
825
|
});
|
|
696
826
|
z.union([UserMessageSchema, AgentMessageSchema]);
|
|
697
827
|
|
|
828
|
+
function buildSessionRuntimeIndex(metadata) {
|
|
829
|
+
if (!metadata) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
const machineId = typeof metadata.machineId === "string" && metadata.machineId ? metadata.machineId : null;
|
|
833
|
+
const hostPid = typeof metadata.hostPid === "number" && Number.isInteger(metadata.hostPid) ? metadata.hostPid : null;
|
|
834
|
+
const startedBy = typeof metadata.startedBy === "string" && metadata.startedBy ? metadata.startedBy : null;
|
|
835
|
+
const lifecycleState = typeof metadata.lifecycleState === "string" && metadata.lifecycleState ? metadata.lifecycleState : null;
|
|
836
|
+
const flavor = typeof metadata.flavor === "string" && metadata.flavor ? metadata.flavor : null;
|
|
837
|
+
if (!machineId && hostPid === null && !startedBy && !lifecycleState && !flavor) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
...machineId ? { machineId } : {},
|
|
842
|
+
...hostPid !== null ? { hostPid } : {},
|
|
843
|
+
...startedBy ? { startedBy } : {},
|
|
844
|
+
...lifecycleState ? { lifecycleState } : {},
|
|
845
|
+
...flavor ? { flavor } : {}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
698
849
|
async function delay(ms) {
|
|
699
850
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
700
851
|
}
|
|
@@ -1501,6 +1652,9 @@ function buildSocketAuth(opts) {
|
|
|
1501
1652
|
|
|
1502
1653
|
const MAX_PENDING_RELIABLE_CODEX_MESSAGES = 200;
|
|
1503
1654
|
const MAX_PENDING_RELIABLE_CODEX_MESSAGE_BYTES = 512 * 1024;
|
|
1655
|
+
const PROTOCOL_V3_INITIAL_SNAPSHOT_LIMIT = 150;
|
|
1656
|
+
const PROTOCOL_V3_CHANGES_PAGE_LIMIT = 200;
|
|
1657
|
+
const PROTOCOL_V3_CAPABILITIES_WAIT_MS = 250;
|
|
1504
1658
|
class ApiSessionClient extends EventEmitter {
|
|
1505
1659
|
credentials;
|
|
1506
1660
|
sessionId;
|
|
@@ -1520,6 +1674,15 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1520
1674
|
pendingReliableCodexMessageBytes = 0;
|
|
1521
1675
|
reconnectAfterServerDisconnectTimer = null;
|
|
1522
1676
|
lastSocketServerError = null;
|
|
1677
|
+
protocolV3SessionSync = null;
|
|
1678
|
+
protocolV3SessionSyncGeneration = 0;
|
|
1679
|
+
protocolV3SessionSyncInProgress = false;
|
|
1680
|
+
bufferedLiveSessionEventsDuringProtocolSync = [];
|
|
1681
|
+
initialProtocolV3SnapshotComplete = false;
|
|
1682
|
+
lastChangeSeq;
|
|
1683
|
+
protocolV3Descriptor = null;
|
|
1684
|
+
protocolV3SocketCapabilities = null;
|
|
1685
|
+
protocolV3SessionSyncWaitTimer = null;
|
|
1523
1686
|
constructor(credentials, session) {
|
|
1524
1687
|
super();
|
|
1525
1688
|
this.credentials = credentials;
|
|
@@ -1530,6 +1693,7 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1530
1693
|
this.agentStateVersion = session.agentStateVersion;
|
|
1531
1694
|
this.encryptionKey = session.encryptionKey;
|
|
1532
1695
|
this.encryptionVariant = session.encryptionVariant;
|
|
1696
|
+
this.lastChangeSeq = session.seq;
|
|
1533
1697
|
this.rpcHandlerManager = new RpcHandlerManager({
|
|
1534
1698
|
scopePrefix: this.sessionId,
|
|
1535
1699
|
encryptionKey: this.encryptionKey,
|
|
@@ -1558,6 +1722,9 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1558
1722
|
this.lastSocketServerError = null;
|
|
1559
1723
|
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
1560
1724
|
this.flushReliableCodexMessages();
|
|
1725
|
+
this.protocolV3Descriptor = null;
|
|
1726
|
+
this.protocolV3SocketCapabilities = null;
|
|
1727
|
+
this.scheduleProtocolV3SessionSync();
|
|
1561
1728
|
});
|
|
1562
1729
|
this.socket.on("rpc-request", async (data, callback) => {
|
|
1563
1730
|
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
@@ -1565,6 +1732,7 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1565
1732
|
this.socket.on("disconnect", (reason) => {
|
|
1566
1733
|
logger.debug("[API] Socket disconnected:", reason);
|
|
1567
1734
|
this.rpcHandlerManager.onSocketDisconnect();
|
|
1735
|
+
this.invalidateProtocolV3SessionSync();
|
|
1568
1736
|
this.retryAfterServerDisconnect(reason);
|
|
1569
1737
|
});
|
|
1570
1738
|
this.socket.on("connect_error", (error) => {
|
|
@@ -1574,43 +1742,42 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1574
1742
|
this.socket.on("update", (data) => {
|
|
1575
1743
|
try {
|
|
1576
1744
|
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
|
|
1577
|
-
if (
|
|
1578
|
-
|
|
1745
|
+
if (this.protocolV3SessionSyncInProgress) {
|
|
1746
|
+
this.bufferLiveSessionEvent({ type: "update", payload: data });
|
|
1579
1747
|
return;
|
|
1580
1748
|
}
|
|
1581
|
-
|
|
1582
|
-
const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
|
|
1583
|
-
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
1584
|
-
const userResult = UserMessageSchema.safeParse(body);
|
|
1585
|
-
if (userResult.success) {
|
|
1586
|
-
if (this.pendingMessageCallback) {
|
|
1587
|
-
this.pendingMessageCallback(userResult.data);
|
|
1588
|
-
} else {
|
|
1589
|
-
this.pendingMessages.push(userResult.data);
|
|
1590
|
-
}
|
|
1591
|
-
} else {
|
|
1592
|
-
this.emit("message", body);
|
|
1593
|
-
}
|
|
1594
|
-
} else if (data.body.t === "update-session") {
|
|
1595
|
-
if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
|
|
1596
|
-
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.metadata.value));
|
|
1597
|
-
this.metadataVersion = data.body.metadata.version;
|
|
1598
|
-
this.emit("metadata-updated", this.metadata);
|
|
1599
|
-
}
|
|
1600
|
-
if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
|
|
1601
|
-
this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.agentState.value)) : null;
|
|
1602
|
-
this.agentStateVersion = data.body.agentState.version;
|
|
1603
|
-
this.emit("agent-state-updated", this.agentState);
|
|
1604
|
-
}
|
|
1605
|
-
} else if (data.body.t === "update-machine") {
|
|
1606
|
-
logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
|
|
1607
|
-
} else {
|
|
1608
|
-
this.emit("message", data.body);
|
|
1609
|
-
}
|
|
1749
|
+
this.handleSocketUpdate(data);
|
|
1610
1750
|
} catch (error) {
|
|
1611
1751
|
logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
|
|
1612
1752
|
}
|
|
1613
1753
|
});
|
|
1754
|
+
this.socket.on("capabilities", (data) => {
|
|
1755
|
+
try {
|
|
1756
|
+
this.protocolV3SocketCapabilities = data;
|
|
1757
|
+
this.protocolV3Descriptor = data.protocol;
|
|
1758
|
+
logger.debug("[API] Received websocket protocol capabilities", {
|
|
1759
|
+
sessionId: this.sessionId,
|
|
1760
|
+
connectionType: data.connectionType,
|
|
1761
|
+
capabilities: data.protocol.capabilities
|
|
1762
|
+
});
|
|
1763
|
+
this.clearProtocolV3SessionSyncWaitTimer();
|
|
1764
|
+
this.scheduleProtocolV3SessionSync({ waitForCapabilities: false });
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
logger.debug("[SOCKET] [CAPABILITIES] [ERROR] Error handling capabilities event", { error });
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
this.socket.on("session-change", (data) => {
|
|
1770
|
+
try {
|
|
1771
|
+
logger.debugLargeJson("[SOCKET] [SESSION-CHANGE] Received session change:", data);
|
|
1772
|
+
if (this.protocolV3SessionSyncInProgress) {
|
|
1773
|
+
this.bufferLiveSessionEvent({ type: "session-change", payload: data });
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
this.handleProtocolV3SessionChangeEvent(data);
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
logger.debug("[SOCKET] [SESSION-CHANGE] [ERROR] Error handling session change", { error });
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1614
1781
|
this.socket.on("error", (error) => {
|
|
1615
1782
|
logger.debug("[API] Socket error:", error);
|
|
1616
1783
|
this.lastSocketServerError = this.normalizeSocketError(error);
|
|
@@ -1623,6 +1790,20 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1623
1790
|
callback(this.pendingMessages.shift());
|
|
1624
1791
|
}
|
|
1625
1792
|
}
|
|
1793
|
+
configureProtocolV3Sync(options) {
|
|
1794
|
+
this.invalidateProtocolV3SessionSync();
|
|
1795
|
+
this.protocolV3SessionSync = options;
|
|
1796
|
+
this.initialProtocolV3SnapshotComplete = false;
|
|
1797
|
+
if (this.protocolV3SessionSync && this.socket.connected) {
|
|
1798
|
+
this.scheduleProtocolV3SessionSync();
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
getProtocolV3DescriptorSnapshot() {
|
|
1802
|
+
return this.protocolV3Descriptor;
|
|
1803
|
+
}
|
|
1804
|
+
getProtocolV3SocketCapabilitiesSnapshot() {
|
|
1805
|
+
return this.protocolV3SocketCapabilities;
|
|
1806
|
+
}
|
|
1626
1807
|
getMetadataSnapshot() {
|
|
1627
1808
|
return this.metadata;
|
|
1628
1809
|
}
|
|
@@ -1781,11 +1962,13 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1781
1962
|
if (process.env.DEBUG) {
|
|
1782
1963
|
logger.debug(`[API] Sending keep alive message: ${thinking}`);
|
|
1783
1964
|
}
|
|
1965
|
+
const sessionIndex = buildSessionRuntimeIndex(this.metadata);
|
|
1784
1966
|
this.socket.volatile.emit("session-alive", {
|
|
1785
1967
|
sid: this.sessionId,
|
|
1786
1968
|
time: Date.now(),
|
|
1787
1969
|
thinking,
|
|
1788
|
-
mode
|
|
1970
|
+
mode,
|
|
1971
|
+
...sessionIndex ? { sessionIndex } : {}
|
|
1789
1972
|
});
|
|
1790
1973
|
}
|
|
1791
1974
|
/**
|
|
@@ -1827,7 +2010,13 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1827
2010
|
this.metadataLock.inLock(async () => {
|
|
1828
2011
|
await backoff(async () => {
|
|
1829
2012
|
let updated = handler(this.metadata);
|
|
1830
|
-
const
|
|
2013
|
+
const sessionIndex = buildSessionRuntimeIndex(updated);
|
|
2014
|
+
const answer = await this.socket.emitWithAck("update-metadata", {
|
|
2015
|
+
sid: this.sessionId,
|
|
2016
|
+
expectedVersion: this.metadataVersion,
|
|
2017
|
+
metadata: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)),
|
|
2018
|
+
...sessionIndex ? { sessionIndex } : {}
|
|
2019
|
+
});
|
|
1831
2020
|
if (answer.result === "success") {
|
|
1832
2021
|
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
|
|
1833
2022
|
this.metadataVersion = answer.version;
|
|
@@ -1888,8 +2077,280 @@ class ApiSessionClient extends EventEmitter {
|
|
|
1888
2077
|
async close() {
|
|
1889
2078
|
logger.debug("[API] socket.close() called");
|
|
1890
2079
|
this.clearReconnectAfterServerDisconnectTimer();
|
|
2080
|
+
this.invalidateProtocolV3SessionSync();
|
|
1891
2081
|
this.socket.close();
|
|
1892
2082
|
}
|
|
2083
|
+
handleSocketUpdate(data) {
|
|
2084
|
+
if (!data.body) {
|
|
2085
|
+
logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
|
|
2089
|
+
const messageSeq = typeof data.body.message.seq === "number" ? data.body.message.seq : null;
|
|
2090
|
+
if (messageSeq !== null && messageSeq <= this.lastChangeSeq) {
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
this.dispatchEncryptedSessionMessage(data.body.message.content.c);
|
|
2094
|
+
if (messageSeq !== null) {
|
|
2095
|
+
this.lastChangeSeq = Math.max(this.lastChangeSeq, messageSeq);
|
|
2096
|
+
}
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
if (data.body.t === "update-session") {
|
|
2100
|
+
const sessionBody = data.body;
|
|
2101
|
+
const changeSeq = typeof sessionBody.changeSeq === "number" ? sessionBody.changeSeq : null;
|
|
2102
|
+
if (changeSeq !== null && changeSeq <= this.lastChangeSeq) {
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (sessionBody.metadata && sessionBody.metadata.version > this.metadataVersion) {
|
|
2106
|
+
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(sessionBody.metadata.value));
|
|
2107
|
+
this.metadataVersion = sessionBody.metadata.version;
|
|
2108
|
+
this.emit("metadata-updated", this.metadata);
|
|
2109
|
+
}
|
|
2110
|
+
if (sessionBody.agentState && sessionBody.agentState.version > this.agentStateVersion) {
|
|
2111
|
+
this.agentState = sessionBody.agentState.value ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(sessionBody.agentState.value)) : null;
|
|
2112
|
+
this.agentStateVersion = sessionBody.agentState.version;
|
|
2113
|
+
this.emit("agent-state-updated", this.agentState);
|
|
2114
|
+
}
|
|
2115
|
+
if (changeSeq !== null) {
|
|
2116
|
+
this.lastChangeSeq = Math.max(this.lastChangeSeq, changeSeq);
|
|
2117
|
+
}
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
if (data.body.t === "update-machine") {
|
|
2121
|
+
logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
if (data.body.t === "delete-session") {
|
|
2125
|
+
const deleteBody = data.body;
|
|
2126
|
+
const changeSeq = typeof deleteBody.changeSeq === "number" ? deleteBody.changeSeq : null;
|
|
2127
|
+
if (changeSeq !== null) {
|
|
2128
|
+
if (changeSeq <= this.lastChangeSeq) {
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
this.lastChangeSeq = Math.max(this.lastChangeSeq, changeSeq);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
this.emit("message", data.body);
|
|
2135
|
+
}
|
|
2136
|
+
dispatchEncryptedSessionMessage(encryptedMessage) {
|
|
2137
|
+
const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(encryptedMessage));
|
|
2138
|
+
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
2139
|
+
const userResult = UserMessageSchema.safeParse(body);
|
|
2140
|
+
if (userResult.success) {
|
|
2141
|
+
if (this.pendingMessageCallback) {
|
|
2142
|
+
this.pendingMessageCallback(userResult.data);
|
|
2143
|
+
} else {
|
|
2144
|
+
this.pendingMessages.push(userResult.data);
|
|
2145
|
+
}
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
this.emit("message", body);
|
|
2149
|
+
}
|
|
2150
|
+
invalidateProtocolV3SessionSync() {
|
|
2151
|
+
this.protocolV3SessionSyncGeneration += 1;
|
|
2152
|
+
this.protocolV3SessionSyncInProgress = false;
|
|
2153
|
+
this.bufferedLiveSessionEventsDuringProtocolSync = [];
|
|
2154
|
+
this.protocolV3Descriptor = null;
|
|
2155
|
+
this.protocolV3SocketCapabilities = null;
|
|
2156
|
+
this.clearProtocolV3SessionSyncWaitTimer();
|
|
2157
|
+
}
|
|
2158
|
+
scheduleProtocolV3SessionSync(options) {
|
|
2159
|
+
if (!this.protocolV3SessionSync || !this.socket.connected) {
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
const shouldWaitForCapabilities = options?.waitForCapabilities ?? (Boolean(this.credentials.signing) && this.protocolV3Descriptor === null);
|
|
2163
|
+
if (shouldWaitForCapabilities) {
|
|
2164
|
+
this.clearProtocolV3SessionSyncWaitTimer();
|
|
2165
|
+
this.protocolV3SessionSyncWaitTimer = setTimeout(() => {
|
|
2166
|
+
this.protocolV3SessionSyncWaitTimer = null;
|
|
2167
|
+
void this.runProtocolV3SessionSync();
|
|
2168
|
+
}, PROTOCOL_V3_CAPABILITIES_WAIT_MS);
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
void this.runProtocolV3SessionSync();
|
|
2172
|
+
}
|
|
2173
|
+
clearProtocolV3SessionSyncWaitTimer() {
|
|
2174
|
+
if (!this.protocolV3SessionSyncWaitTimer) {
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
clearTimeout(this.protocolV3SessionSyncWaitTimer);
|
|
2178
|
+
this.protocolV3SessionSyncWaitTimer = null;
|
|
2179
|
+
}
|
|
2180
|
+
async runProtocolV3SessionSync() {
|
|
2181
|
+
if (!this.protocolV3SessionSync || this.protocolV3SessionSyncInProgress || !this.socket.connected) {
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (this.protocolV3Descriptor && !this.supportsKnownProtocolCapability("session_changes_v3")) {
|
|
2185
|
+
logger.debug("[API] Skipping protocol v3 session catch-up because websocket capabilities do not advertise session_changes_v3", {
|
|
2186
|
+
sessionId: this.sessionId
|
|
2187
|
+
});
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const generation = this.protocolV3SessionSyncGeneration + 1;
|
|
2191
|
+
this.protocolV3SessionSyncGeneration = generation;
|
|
2192
|
+
this.protocolV3SessionSyncInProgress = true;
|
|
2193
|
+
this.bufferedLiveSessionEventsDuringProtocolSync = [];
|
|
2194
|
+
try {
|
|
2195
|
+
logger.debug("[API] Starting protocol v3 session catch-up", {
|
|
2196
|
+
sessionId: this.sessionId,
|
|
2197
|
+
lastChangeSeq: this.lastChangeSeq
|
|
2198
|
+
});
|
|
2199
|
+
if (!this.initialProtocolV3SnapshotComplete && this.lastChangeSeq === 0 && (!this.protocolV3Descriptor || this.supportsKnownProtocolCapability("session_snapshot_v3"))) {
|
|
2200
|
+
const snapshot = await this.protocolV3SessionSync.getSessionSnapshot(
|
|
2201
|
+
this.sessionId,
|
|
2202
|
+
this.protocolV3SessionSync.snapshotLimit ?? PROTOCOL_V3_INITIAL_SNAPSHOT_LIMIT
|
|
2203
|
+
);
|
|
2204
|
+
if (!this.isActiveProtocolV3SessionSync(generation)) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
if (snapshot) {
|
|
2208
|
+
this.applyProtocolV3Snapshot(snapshot);
|
|
2209
|
+
this.initialProtocolV3SnapshotComplete = true;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
while (this.isActiveProtocolV3SessionSync(generation)) {
|
|
2213
|
+
const afterSeq = this.lastChangeSeq;
|
|
2214
|
+
const page = await this.protocolV3SessionSync.getSessionChanges(
|
|
2215
|
+
this.sessionId,
|
|
2216
|
+
afterSeq,
|
|
2217
|
+
this.protocolV3SessionSync.changesLimit ?? PROTOCOL_V3_CHANGES_PAGE_LIMIT
|
|
2218
|
+
);
|
|
2219
|
+
if (!this.isActiveProtocolV3SessionSync(generation)) {
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
if (!page) {
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
this.applyProtocolV3SessionOverlay(page.session);
|
|
2226
|
+
for (const change of page.changes) {
|
|
2227
|
+
this.applyProtocolV3Change(change);
|
|
2228
|
+
}
|
|
2229
|
+
if (!page.cursor.hasMore || page.changes.length === 0) {
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
logger.debug("[API] Protocol v3 session catch-up failed", {
|
|
2235
|
+
sessionId: this.sessionId,
|
|
2236
|
+
error
|
|
2237
|
+
});
|
|
2238
|
+
} finally {
|
|
2239
|
+
if (generation !== this.protocolV3SessionSyncGeneration) {
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
this.protocolV3SessionSyncInProgress = false;
|
|
2243
|
+
const bufferedLiveEvents = this.bufferedLiveSessionEventsDuringProtocolSync;
|
|
2244
|
+
this.bufferedLiveSessionEventsDuringProtocolSync = [];
|
|
2245
|
+
for (const liveEvent of bufferedLiveEvents) {
|
|
2246
|
+
this.replayBufferedLiveSessionEvent(liveEvent);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
isActiveProtocolV3SessionSync(generation) {
|
|
2251
|
+
return generation === this.protocolV3SessionSyncGeneration && this.socket.connected;
|
|
2252
|
+
}
|
|
2253
|
+
applyProtocolV3Snapshot(snapshot) {
|
|
2254
|
+
this.applyProtocolV3SessionOverlay(snapshot.session);
|
|
2255
|
+
for (const message of snapshot.snapshot.messages) {
|
|
2256
|
+
if (message.seq <= this.lastChangeSeq) {
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
if (message.content.t === "encrypted") {
|
|
2260
|
+
this.dispatchEncryptedSessionMessage(message.content.c);
|
|
2261
|
+
}
|
|
2262
|
+
this.lastChangeSeq = Math.max(this.lastChangeSeq, message.seq);
|
|
2263
|
+
}
|
|
2264
|
+
this.lastChangeSeq = Math.max(this.lastChangeSeq, snapshot.session.lastChangeSeq);
|
|
2265
|
+
}
|
|
2266
|
+
applyProtocolV3SessionOverlay(session) {
|
|
2267
|
+
if (typeof session.metadataVersion === "number" && typeof session.metadata === "string" && session.metadataVersion > this.metadataVersion) {
|
|
2268
|
+
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(session.metadata));
|
|
2269
|
+
this.metadataVersion = session.metadataVersion;
|
|
2270
|
+
this.emit("metadata-updated", this.metadata);
|
|
2271
|
+
}
|
|
2272
|
+
if (typeof session.agentStateVersion === "number" && session.agentStateVersion > this.agentStateVersion) {
|
|
2273
|
+
this.agentState = typeof session.agentState === "string" ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(session.agentState)) : null;
|
|
2274
|
+
this.agentStateVersion = session.agentStateVersion;
|
|
2275
|
+
this.emit("agent-state-updated", this.agentState);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
applyProtocolV3Change(change) {
|
|
2279
|
+
if (change.changeSeq <= this.lastChangeSeq) {
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const payload = change.payload && typeof change.payload === "object" ? change.payload : {};
|
|
2283
|
+
switch (change.changeType) {
|
|
2284
|
+
case "message.created": {
|
|
2285
|
+
const message = payload.message;
|
|
2286
|
+
if (message && typeof message === "object") {
|
|
2287
|
+
const content = message.content;
|
|
2288
|
+
if (content && typeof content === "object" && content.t === "encrypted" && typeof content.c === "string") {
|
|
2289
|
+
this.dispatchEncryptedSessionMessage(content.c);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
2294
|
+
case "session.metadata.updated": {
|
|
2295
|
+
const metadataUpdate = payload.metadata;
|
|
2296
|
+
if (metadataUpdate && typeof metadataUpdate === "object" && typeof metadataUpdate.version === "number" && typeof metadataUpdate.value === "string") {
|
|
2297
|
+
const version = metadataUpdate.version;
|
|
2298
|
+
if (version <= this.metadataVersion) {
|
|
2299
|
+
break;
|
|
2300
|
+
}
|
|
2301
|
+
this.metadata = decrypt(
|
|
2302
|
+
this.encryptionKey,
|
|
2303
|
+
this.encryptionVariant,
|
|
2304
|
+
decodeBase64(metadataUpdate.value)
|
|
2305
|
+
);
|
|
2306
|
+
this.metadataVersion = version;
|
|
2307
|
+
this.emit("metadata-updated", this.metadata);
|
|
2308
|
+
}
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
case "session.agent-state.updated": {
|
|
2312
|
+
const agentStateUpdate = payload.agentState;
|
|
2313
|
+
if (agentStateUpdate && typeof agentStateUpdate === "object" && typeof agentStateUpdate.version === "number") {
|
|
2314
|
+
const version = agentStateUpdate.version;
|
|
2315
|
+
if (version <= this.agentStateVersion) {
|
|
2316
|
+
break;
|
|
2317
|
+
}
|
|
2318
|
+
const encodedAgentState = agentStateUpdate.value;
|
|
2319
|
+
this.agentState = typeof encodedAgentState === "string" ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(encodedAgentState)) : null;
|
|
2320
|
+
this.agentStateVersion = version;
|
|
2321
|
+
this.emit("agent-state-updated", this.agentState);
|
|
2322
|
+
}
|
|
2323
|
+
break;
|
|
2324
|
+
}
|
|
2325
|
+
case "session.deleted":
|
|
2326
|
+
this.emit("message", {
|
|
2327
|
+
t: "delete-session",
|
|
2328
|
+
sid: this.sessionId,
|
|
2329
|
+
changeSeq: change.changeSeq
|
|
2330
|
+
});
|
|
2331
|
+
break;
|
|
2332
|
+
}
|
|
2333
|
+
this.lastChangeSeq = Math.max(this.lastChangeSeq, change.changeSeq);
|
|
2334
|
+
}
|
|
2335
|
+
handleProtocolV3SessionChangeEvent(data) {
|
|
2336
|
+
if (data.sessionId !== this.sessionId) {
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
this.applyProtocolV3Change(data.change);
|
|
2340
|
+
}
|
|
2341
|
+
replayBufferedLiveSessionEvent(event) {
|
|
2342
|
+
if (event.type === "update") {
|
|
2343
|
+
this.handleSocketUpdate(event.payload);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
this.handleProtocolV3SessionChangeEvent(event.payload);
|
|
2347
|
+
}
|
|
2348
|
+
bufferLiveSessionEvent(event) {
|
|
2349
|
+
this.bufferedLiveSessionEventsDuringProtocolSync.push(event);
|
|
2350
|
+
}
|
|
2351
|
+
supportsKnownProtocolCapability(capability) {
|
|
2352
|
+
return Array.isArray(this.protocolV3Descriptor?.capabilities) && this.protocolV3Descriptor.capabilities.includes(capability);
|
|
2353
|
+
}
|
|
1893
2354
|
emitEncryptedSessionMessage(encrypted) {
|
|
1894
2355
|
this.socket.emit("message", {
|
|
1895
2356
|
sid: this.sessionId,
|
|
@@ -2198,6 +2659,141 @@ class ApiMachineClient {
|
|
|
2198
2659
|
}
|
|
2199
2660
|
}
|
|
2200
2661
|
|
|
2662
|
+
class ApiUserObserverClient extends EventEmitter {
|
|
2663
|
+
constructor(credentials) {
|
|
2664
|
+
super();
|
|
2665
|
+
this.credentials = credentials;
|
|
2666
|
+
this.socket = io(configuration.serverUrl, {
|
|
2667
|
+
auth: (cb) => cb(buildSocketAuth({
|
|
2668
|
+
credentials: this.credentials,
|
|
2669
|
+
clientType: "user-scoped"
|
|
2670
|
+
})),
|
|
2671
|
+
path: "/v1/updates",
|
|
2672
|
+
reconnection: true,
|
|
2673
|
+
reconnectionAttempts: Infinity,
|
|
2674
|
+
reconnectionDelay: 1e3,
|
|
2675
|
+
reconnectionDelayMax: 5e3,
|
|
2676
|
+
transports: ["websocket"],
|
|
2677
|
+
withCredentials: true,
|
|
2678
|
+
autoConnect: false
|
|
2679
|
+
});
|
|
2680
|
+
this.socket.on("connect", () => {
|
|
2681
|
+
logger.debug("[API USER OBSERVER] Socket connected successfully");
|
|
2682
|
+
this.clearReconnectAfterServerDisconnectTimer();
|
|
2683
|
+
this.lastSocketServerError = null;
|
|
2684
|
+
this.protocolV3Descriptor = null;
|
|
2685
|
+
this.protocolV3SocketCapabilities = null;
|
|
2686
|
+
});
|
|
2687
|
+
this.socket.on("disconnect", (reason) => {
|
|
2688
|
+
logger.debug("[API USER OBSERVER] Socket disconnected:", reason);
|
|
2689
|
+
this.protocolV3Descriptor = null;
|
|
2690
|
+
this.protocolV3SocketCapabilities = null;
|
|
2691
|
+
this.retryAfterServerDisconnect(reason);
|
|
2692
|
+
});
|
|
2693
|
+
this.socket.on("connect_error", (error) => {
|
|
2694
|
+
logger.debug("[API USER OBSERVER] Socket connection error:", error);
|
|
2695
|
+
});
|
|
2696
|
+
this.socket.on("capabilities", (data) => {
|
|
2697
|
+
this.protocolV3SocketCapabilities = data;
|
|
2698
|
+
this.protocolV3Descriptor = data.protocol;
|
|
2699
|
+
this.emit("protocol-capabilities", data);
|
|
2700
|
+
});
|
|
2701
|
+
this.socket.on("update", (data) => {
|
|
2702
|
+
this.emit("update", data);
|
|
2703
|
+
});
|
|
2704
|
+
this.socket.on("ephemeral", (data) => {
|
|
2705
|
+
this.emit("ephemeral", data);
|
|
2706
|
+
});
|
|
2707
|
+
this.socket.on("error", (error) => {
|
|
2708
|
+
logger.debug("[API USER OBSERVER] Socket error:", error);
|
|
2709
|
+
this.lastSocketServerError = this.normalizeSocketError(error);
|
|
2710
|
+
});
|
|
2711
|
+
this.socket.connect();
|
|
2712
|
+
}
|
|
2713
|
+
socket;
|
|
2714
|
+
protocolV3Descriptor = null;
|
|
2715
|
+
protocolV3SocketCapabilities = null;
|
|
2716
|
+
reconnectAfterServerDisconnectTimer = null;
|
|
2717
|
+
lastSocketServerError = null;
|
|
2718
|
+
isConnected() {
|
|
2719
|
+
return this.socket.connected;
|
|
2720
|
+
}
|
|
2721
|
+
getProtocolV3DescriptorSnapshot() {
|
|
2722
|
+
return this.protocolV3Descriptor;
|
|
2723
|
+
}
|
|
2724
|
+
getProtocolV3SocketCapabilitiesSnapshot() {
|
|
2725
|
+
return this.protocolV3SocketCapabilities;
|
|
2726
|
+
}
|
|
2727
|
+
onEphemeral(callback) {
|
|
2728
|
+
this.on("ephemeral", callback);
|
|
2729
|
+
}
|
|
2730
|
+
offEphemeral(callback) {
|
|
2731
|
+
this.off("ephemeral", callback);
|
|
2732
|
+
}
|
|
2733
|
+
onUpdate(callback) {
|
|
2734
|
+
this.on("update", callback);
|
|
2735
|
+
}
|
|
2736
|
+
offUpdate(callback) {
|
|
2737
|
+
this.off("update", callback);
|
|
2738
|
+
}
|
|
2739
|
+
async close() {
|
|
2740
|
+
this.clearReconnectAfterServerDisconnectTimer();
|
|
2741
|
+
this.protocolV3Descriptor = null;
|
|
2742
|
+
this.protocolV3SocketCapabilities = null;
|
|
2743
|
+
this.socket.close();
|
|
2744
|
+
}
|
|
2745
|
+
retryAfterServerDisconnect(reason) {
|
|
2746
|
+
if (reason !== "io server disconnect") {
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
const errorCode = this.lastSocketServerError?.error;
|
|
2750
|
+
if (errorCode !== "REQUEST_EXPIRED" && errorCode !== "REQUEST_REPLAYED") {
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
if (this.reconnectAfterServerDisconnectTimer || this.socket.connected) {
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
logger.debug("[API USER OBSERVER] Scheduling manual reconnect after retryable server disconnect", {
|
|
2757
|
+
reason,
|
|
2758
|
+
errorCode,
|
|
2759
|
+
message: this.lastSocketServerError?.message
|
|
2760
|
+
});
|
|
2761
|
+
this.reconnectAfterServerDisconnectTimer = setTimeout(() => {
|
|
2762
|
+
this.reconnectAfterServerDisconnectTimer = null;
|
|
2763
|
+
if (this.socket.connected) {
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
logger.debug("[API USER OBSERVER] Retrying socket connection after server disconnect", {
|
|
2767
|
+
errorCode,
|
|
2768
|
+
message: this.lastSocketServerError?.message
|
|
2769
|
+
});
|
|
2770
|
+
this.socket.connect();
|
|
2771
|
+
}, 1e3);
|
|
2772
|
+
}
|
|
2773
|
+
clearReconnectAfterServerDisconnectTimer() {
|
|
2774
|
+
if (!this.reconnectAfterServerDisconnectTimer) {
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
clearTimeout(this.reconnectAfterServerDisconnectTimer);
|
|
2778
|
+
this.reconnectAfterServerDisconnectTimer = null;
|
|
2779
|
+
}
|
|
2780
|
+
normalizeSocketError(error) {
|
|
2781
|
+
if (!error || typeof error !== "object") {
|
|
2782
|
+
return null;
|
|
2783
|
+
}
|
|
2784
|
+
const candidate = error;
|
|
2785
|
+
const errorCode = typeof candidate.error === "string" ? candidate.error : void 0;
|
|
2786
|
+
const message = typeof candidate.message === "string" ? candidate.message : void 0;
|
|
2787
|
+
if (!errorCode && !message) {
|
|
2788
|
+
return null;
|
|
2789
|
+
}
|
|
2790
|
+
return {
|
|
2791
|
+
error: errorCode,
|
|
2792
|
+
message
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2201
2797
|
class PushNotificationClient {
|
|
2202
2798
|
credentials;
|
|
2203
2799
|
baseUrl;
|
|
@@ -2518,6 +3114,39 @@ class ApiClient {
|
|
|
2518
3114
|
}
|
|
2519
3115
|
return new AuthenticationRequiredError(AUTHENTICATION_REQUIRED_MESSAGE);
|
|
2520
3116
|
}
|
|
3117
|
+
async requestProtocolV3Resource(opts) {
|
|
3118
|
+
if (!this.credential.signing) {
|
|
3119
|
+
logger.debug(`[API] Skipping ${opts.logLabel} because signing credentials are unavailable`);
|
|
3120
|
+
return null;
|
|
3121
|
+
}
|
|
3122
|
+
try {
|
|
3123
|
+
const response = await this.request({
|
|
3124
|
+
method: "GET",
|
|
3125
|
+
url: opts.url,
|
|
3126
|
+
timeout: 5e3
|
|
3127
|
+
});
|
|
3128
|
+
const parsed = opts.schema.safeParse(response.data);
|
|
3129
|
+
if (!parsed.success) {
|
|
3130
|
+
logger.debug(`[API] ${opts.logLabel} returned an unexpected payload`, response.data);
|
|
3131
|
+
return null;
|
|
3132
|
+
}
|
|
3133
|
+
return parsed.data;
|
|
3134
|
+
} catch (error) {
|
|
3135
|
+
if (axios.isAxiosError(error)) {
|
|
3136
|
+
const status = error.response?.status;
|
|
3137
|
+
if (status === 401 || status === 403 || status === 404) {
|
|
3138
|
+
logger.debug(`[API] ${opts.logLabel} unavailable (${status ?? "unknown"})`);
|
|
3139
|
+
return null;
|
|
3140
|
+
}
|
|
3141
|
+
if (error.code && isNetworkError(error.code)) {
|
|
3142
|
+
logger.debug(`[API] ${opts.logLabel} probe failed due to network error`, error.code);
|
|
3143
|
+
return null;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
logger.debug(`[API] Failed to fetch ${opts.logLabel}`, error);
|
|
3147
|
+
return null;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
2521
3150
|
async request(opts) {
|
|
2522
3151
|
return axios.request({
|
|
2523
3152
|
method: opts.method,
|
|
@@ -2536,6 +3165,72 @@ class ApiClient {
|
|
|
2536
3165
|
timeout: opts.timeout
|
|
2537
3166
|
});
|
|
2538
3167
|
}
|
|
3168
|
+
async getProtocolV3Descriptor() {
|
|
3169
|
+
const url = `${configuration.serverUrl}/v3/capabilities`;
|
|
3170
|
+
const payload = await this.requestProtocolV3Resource({
|
|
3171
|
+
url,
|
|
3172
|
+
schema: ProtocolV3CapabilitiesResponseSchema,
|
|
3173
|
+
logLabel: "/v3/capabilities"
|
|
3174
|
+
});
|
|
3175
|
+
return payload?.protocol ?? null;
|
|
3176
|
+
}
|
|
3177
|
+
async getSessionSnapshotV3(sessionId, limit = 150) {
|
|
3178
|
+
const url = new URL(`/v3/sessions/${encodeURIComponent(sessionId)}/snapshot`, configuration.serverUrl);
|
|
3179
|
+
url.searchParams.set("limit", String(limit));
|
|
3180
|
+
return await this.requestProtocolV3Resource({
|
|
3181
|
+
url: url.toString(),
|
|
3182
|
+
schema: ProtocolV3SessionSnapshotResponseSchema,
|
|
3183
|
+
logLabel: `/v3/sessions/${sessionId}/snapshot`
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
async getSessionChangesV3(sessionId, afterSeq, limit = 200) {
|
|
3187
|
+
const url = new URL(`/v3/sessions/${encodeURIComponent(sessionId)}/changes`, configuration.serverUrl);
|
|
3188
|
+
url.searchParams.set("afterSeq", String(afterSeq));
|
|
3189
|
+
url.searchParams.set("limit", String(limit));
|
|
3190
|
+
return await this.requestProtocolV3Resource({
|
|
3191
|
+
url: url.toString(),
|
|
3192
|
+
schema: ProtocolV3SessionChangesResponseSchema,
|
|
3193
|
+
logLabel: `/v3/sessions/${sessionId}/changes`
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
async listSessionsIndex() {
|
|
3197
|
+
try {
|
|
3198
|
+
const response = await this.request({
|
|
3199
|
+
method: "GET",
|
|
3200
|
+
url: `${configuration.serverUrl}/v1/sessions`,
|
|
3201
|
+
timeout: 5e3
|
|
3202
|
+
});
|
|
3203
|
+
const parsed = SessionListResponseSchema.safeParse(response.data);
|
|
3204
|
+
if (!parsed.success) {
|
|
3205
|
+
logger.debug("[API] /v1/sessions returned an unexpected payload while listing session index", response.data);
|
|
3206
|
+
return [];
|
|
3207
|
+
}
|
|
3208
|
+
return parsed.data.sessions.map((session) => ({
|
|
3209
|
+
id: session.id,
|
|
3210
|
+
seq: session.seq,
|
|
3211
|
+
title: session.title ?? null,
|
|
3212
|
+
active: session.active,
|
|
3213
|
+
activeAt: session.activeAt,
|
|
3214
|
+
createdAt: session.createdAt,
|
|
3215
|
+
updatedAt: session.updatedAt,
|
|
3216
|
+
sessionIndex: session.sessionIndex ?? null
|
|
3217
|
+
}));
|
|
3218
|
+
} catch (error) {
|
|
3219
|
+
if (axios.isAxiosError(error)) {
|
|
3220
|
+
const status = error.response?.status;
|
|
3221
|
+
if (status === 401 || status === 403 || status === 404) {
|
|
3222
|
+
logger.debug(`[API] Session index unavailable (${status ?? "unknown"})`);
|
|
3223
|
+
return [];
|
|
3224
|
+
}
|
|
3225
|
+
if (error.code && isNetworkError(error.code)) {
|
|
3226
|
+
logger.debug("[API] Session index probe failed due to network error", error.code);
|
|
3227
|
+
return [];
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
logger.debug("[API] Failed to list session index", error);
|
|
3231
|
+
return [];
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
2539
3234
|
/**
|
|
2540
3235
|
* Create a new session or load existing one with the given tag
|
|
2541
3236
|
*/
|
|
@@ -2555,6 +3250,7 @@ class ApiClient {
|
|
|
2555
3250
|
encryptionVariant = "legacy";
|
|
2556
3251
|
}
|
|
2557
3252
|
try {
|
|
3253
|
+
const sessionIndex = buildSessionRuntimeIndex(opts.metadata);
|
|
2558
3254
|
const response = await this.request({
|
|
2559
3255
|
method: "POST",
|
|
2560
3256
|
url: `${configuration.serverUrl}/v1/sessions`,
|
|
@@ -2562,7 +3258,8 @@ class ApiClient {
|
|
|
2562
3258
|
tag: opts.tag,
|
|
2563
3259
|
metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
|
|
2564
3260
|
agentState: opts.state ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.state)) : null,
|
|
2565
|
-
dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : null
|
|
3261
|
+
dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : null,
|
|
3262
|
+
...sessionIndex ? { sessionIndex } : {}
|
|
2566
3263
|
},
|
|
2567
3264
|
timeout: 6e4
|
|
2568
3265
|
});
|
|
@@ -2731,11 +3428,21 @@ class ApiClient {
|
|
|
2731
3428
|
}
|
|
2732
3429
|
}
|
|
2733
3430
|
sessionSyncClient(session) {
|
|
2734
|
-
|
|
3431
|
+
const client = new ApiSessionClient(this.credential, session);
|
|
3432
|
+
if (this.credential.signing) {
|
|
3433
|
+
client.configureProtocolV3Sync({
|
|
3434
|
+
getSessionSnapshot: (sessionId, limit) => this.getSessionSnapshotV3(sessionId, limit),
|
|
3435
|
+
getSessionChanges: (sessionId, afterSeq, limit) => this.getSessionChangesV3(sessionId, afterSeq, limit)
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
return client;
|
|
2735
3439
|
}
|
|
2736
3440
|
machineSyncClient(machine) {
|
|
2737
3441
|
return new ApiMachineClient(this.credential, machine);
|
|
2738
3442
|
}
|
|
3443
|
+
userScopedObserverClient() {
|
|
3444
|
+
return new ApiUserObserverClient(this.credential);
|
|
3445
|
+
}
|
|
2739
3446
|
push() {
|
|
2740
3447
|
return this.pushClient;
|
|
2741
3448
|
}
|