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