@gatherthread/dsh-host 0.1.0-alpha.2 → 0.1.0-alpha.4
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/README.md +8 -1
- package/bundle/manifest.json +1 -1
- package/client/client.js +55 -5
- package/dist/bundle/native-plugin.js +178 -14
- package/dist/bundle/plugin.js +121 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ The package does not install a DSH runtime dependency.
|
|
|
19
19
|
After `@gatherthread/dsh-host` is published, the ordinary three-step path is:
|
|
20
20
|
|
|
21
21
|
```text
|
|
22
|
-
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add @gatherthread/dsh-host@0.1.0-alpha.
|
|
22
|
+
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add @gatherthread/dsh-host@0.1.0-alpha.4
|
|
23
23
|
npx @deepseek-ai/dsh@0.1.2-rc.1 web
|
|
24
24
|
# In DSH: Settings -> GatherThread / 共序 -> Sign in and pair
|
|
25
25
|
```
|
|
@@ -159,3 +159,10 @@ same runtime advance the projection cursor but are not appended back to DSH,
|
|
|
159
159
|
while events written through another harness are imported normally. V1
|
|
160
160
|
connector state deliberately restarts only the native projection cursor at zero
|
|
161
161
|
once so history that the earlier poller observed but discarded is backfilled.
|
|
162
|
+
|
|
163
|
+
Automatic upload is stored independently for every bound DSH conversation and
|
|
164
|
+
defaults on. In **Settings -> GatherThread / 共序**, the user can switch it off
|
|
165
|
+
without stopping cloud-to-local projection, or choose **Manual upload** to scan
|
|
166
|
+
and send completed eligible local turns. The manual action is also the recovery
|
|
167
|
+
path after a missed native event or transport interruption, uses the existing
|
|
168
|
+
durable idempotent outbox, and never silently re-enables automatic upload.
|
package/bundle/manifest.json
CHANGED
package/client/client.js
CHANGED
|
@@ -105,7 +105,9 @@ function GatherThreadStatusPanel({ connection, sessions }) {
|
|
|
105
105
|
}
|
|
106
106
|
onSuccess?.(next);
|
|
107
107
|
} catch {
|
|
108
|
-
setNotice("
|
|
108
|
+
setNotice(endpoint.startsWith("sync/")
|
|
109
|
+
? "上传操作未完成。请检查连接和会话权限后重试。"
|
|
110
|
+
: "操作未完成。请检查服务器地址、当前授权和模型配置。");
|
|
109
111
|
} finally {
|
|
110
112
|
setBusy(false);
|
|
111
113
|
}
|
|
@@ -129,7 +131,9 @@ function GatherThreadStatusPanel({ connection, sessions }) {
|
|
|
129
131
|
React.createElement("p", { style: styles.summary },
|
|
130
132
|
`活跃会话 ${String(snapshot?.activeSessionCount ?? 0)} / ${String(sessionRows.length)}`),
|
|
131
133
|
React.createElement("div", { style: styles.list },
|
|
132
|
-
...sessionRows.map((session) =>
|
|
134
|
+
...sessionRows.map((session) => {
|
|
135
|
+
const sync = nativeState?.localSync?.find((item) => item.sessionId === session.sessionId);
|
|
136
|
+
return React.createElement("div", {
|
|
133
137
|
key: session.sessionId,
|
|
134
138
|
style: styles.row,
|
|
135
139
|
},
|
|
@@ -137,7 +141,30 @@ function GatherThreadStatusPanel({ connection, sessions }) {
|
|
|
137
141
|
React.createElement("strong", { style: styles.sessionTitle }, session.title),
|
|
138
142
|
React.createElement("span", { style: styles.sessionMeta },
|
|
139
143
|
session.lastSyncedAt === undefined ? "尚未同步" : `最近同步 ${formatTime(session.lastSyncedAt)}`)),
|
|
140
|
-
React.createElement("span", { style: badgeStyle(session.state) }, statusLabel(session.state))
|
|
144
|
+
sync === undefined ? React.createElement("span", { style: badgeStyle(session.state) }, statusLabel(session.state))
|
|
145
|
+
: React.createElement("div", { style: styles.syncActions },
|
|
146
|
+
React.createElement("label", { style: styles.syncToggle },
|
|
147
|
+
React.createElement("input", {
|
|
148
|
+
type: "checkbox",
|
|
149
|
+
checked: sync.automaticUpload,
|
|
150
|
+
disabled: busy,
|
|
151
|
+
onChange: (event) => void runAction("sync/set-auto-upload", {
|
|
152
|
+
projectId: sync.projectId,
|
|
153
|
+
sessionId: sync.sessionId,
|
|
154
|
+
enabled: event.target.checked,
|
|
155
|
+
}),
|
|
156
|
+
}),
|
|
157
|
+
"自动上传"),
|
|
158
|
+
React.createElement("button", {
|
|
159
|
+
type: "button",
|
|
160
|
+
disabled: busy || sync.uploadableLocalTurns < 1,
|
|
161
|
+
style: styles.compactButton,
|
|
162
|
+
onClick: () => void runAction("sync/upload", {
|
|
163
|
+
projectId: sync.projectId,
|
|
164
|
+
sessionId: sync.sessionId,
|
|
165
|
+
}),
|
|
166
|
+
}, sync.uploadableLocalTurns > 0 ? `手动上传 ${String(sync.uploadableLocalTurns)}` : "已上传")));
|
|
167
|
+
})),
|
|
141
168
|
nativeState === undefined || unavailable
|
|
142
169
|
? null
|
|
143
170
|
: renderNativeControls({
|
|
@@ -386,7 +413,7 @@ function detail(label, value) {
|
|
|
386
413
|
function parseNativeState(value) {
|
|
387
414
|
exactObject(value, [
|
|
388
415
|
"schemaVersion", "integration", "authorization", "compatibility", "runtime",
|
|
389
|
-
"officialServerUrl", "serverUrl", "deviceName", "route", "projectCount", "bindings", "pairing", "recoverableError",
|
|
416
|
+
"officialServerUrl", "serverUrl", "deviceName", "route", "projectCount", "bindings", "localSync", "pairing", "recoverableError",
|
|
390
417
|
]);
|
|
391
418
|
if (value.schemaVersion !== 2 || value.integration !== "gatherthread") throw new Error("invalid native status identity");
|
|
392
419
|
if (!AUTHORIZATION_STATES.has(value.authorization)) throw new Error("invalid native authorization state");
|
|
@@ -409,6 +436,7 @@ function parseNativeState(value) {
|
|
|
409
436
|
if (value.deviceName !== undefined) boundedText(value.deviceName, 120);
|
|
410
437
|
let route;
|
|
411
438
|
let bindings;
|
|
439
|
+
let localSync;
|
|
412
440
|
if (value.route !== undefined || value.bindings !== undefined || value.projectCount !== undefined) {
|
|
413
441
|
if (value.route === undefined || !Number.isSafeInteger(value.projectCount) || value.projectCount < 0
|
|
414
442
|
|| !Array.isArray(value.bindings) || value.bindings.length > 100
|
|
@@ -430,6 +458,25 @@ function parseNativeState(value) {
|
|
|
430
458
|
}
|
|
431
459
|
return { ...binding };
|
|
432
460
|
});
|
|
461
|
+
const localSyncValue = value.localSync ?? [];
|
|
462
|
+
if (!Array.isArray(localSyncValue) || localSyncValue.length > 100) throw new Error("invalid local sync status");
|
|
463
|
+
localSync = localSyncValue.map((sync) => {
|
|
464
|
+
exactObject(sync, [
|
|
465
|
+
"projectId", "projectName", "sessionId", "localSessionId", "title", "automaticUpload",
|
|
466
|
+
"pendingLocalTurns", "uploadableLocalTurns",
|
|
467
|
+
]);
|
|
468
|
+
boundedText(sync.projectId, 128);
|
|
469
|
+
boundedText(sync.projectName, 160);
|
|
470
|
+
boundedText(sync.sessionId, 128);
|
|
471
|
+
boundedText(sync.localSessionId, 200);
|
|
472
|
+
boundedText(sync.title, 160);
|
|
473
|
+
if (typeof sync.automaticUpload !== "boolean"
|
|
474
|
+
|| !Number.isSafeInteger(sync.pendingLocalTurns) || sync.pendingLocalTurns < 0
|
|
475
|
+
|| !Number.isSafeInteger(sync.uploadableLocalTurns) || sync.uploadableLocalTurns < 0) {
|
|
476
|
+
throw new Error("invalid local sync status");
|
|
477
|
+
}
|
|
478
|
+
return { ...sync };
|
|
479
|
+
});
|
|
433
480
|
}
|
|
434
481
|
let pairing;
|
|
435
482
|
if (value.pairing !== undefined) {
|
|
@@ -465,7 +512,7 @@ function parseNativeState(value) {
|
|
|
465
512
|
...(officialServerUrl === undefined ? {} : { officialServerUrl }),
|
|
466
513
|
...(serverUrl === undefined ? {} : { serverUrl }),
|
|
467
514
|
...(value.deviceName === undefined ? {} : { deviceName: value.deviceName }),
|
|
468
|
-
...(route === undefined ? {} : { route, projectCount: value.projectCount, bindings }),
|
|
515
|
+
...(route === undefined ? {} : { route, projectCount: value.projectCount, bindings, localSync }),
|
|
469
516
|
...(pairing === undefined ? {} : { pairing }),
|
|
470
517
|
...(value.recoverableError === undefined ? {} : { recoverableError: value.recoverableError }),
|
|
471
518
|
};
|
|
@@ -592,6 +639,9 @@ const styles = {
|
|
|
592
639
|
sessionText: { minWidth: 0, display: "grid", gap: 3 },
|
|
593
640
|
sessionTitle: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: 14 },
|
|
594
641
|
sessionMeta: { opacity: 0.62, fontSize: 12 },
|
|
642
|
+
syncActions: { flex: "none", display: "grid", justifyItems: "end", gap: 6 },
|
|
643
|
+
syncToggle: { display: "inline-flex", alignItems: "center", gap: 5, fontSize: 12, cursor: "pointer" },
|
|
644
|
+
compactButton: { minHeight: 28, padding: "4px 8px", borderRadius: 8, border: "1px solid rgba(127,127,127,.3)", background: "transparent", color: "inherit", font: "inherit", fontSize: 12, cursor: "pointer" },
|
|
595
645
|
badge: { flex: "none", padding: "4px 8px", borderRadius: 999, fontSize: 12, fontWeight: 600, background: "rgba(127,127,127,.12)", color: "inherit", border: "1px solid rgba(127,127,127,.2)" },
|
|
596
646
|
connectionCard: { display: "grid", gap: 12, padding: "14px", border: "1px solid rgba(127,127,127,.2)", borderRadius: 14, background: "rgba(127,127,127,.06)" },
|
|
597
647
|
subheading: { display: "grid", gap: 4 },
|
|
@@ -191,10 +191,10 @@ var HttpCollaborationClient = class {
|
|
|
191
191
|
const body = requiredObject(await this.#request(`/snapshot-requests/${encodeURIComponent(requestId)}`));
|
|
192
192
|
return fromWireSnapshotRequest(body.snapshot_request ?? body);
|
|
193
193
|
}
|
|
194
|
-
async createSnapshotRequest(sessionId) {
|
|
194
|
+
async createSnapshotRequest(sessionId, kind = "immutable") {
|
|
195
195
|
const body = requiredObject(await this.#request(`/sessions/${encodeURIComponent(sessionId)}/snapshot-requests`, {
|
|
196
196
|
method: "POST",
|
|
197
|
-
body:
|
|
197
|
+
body: JSON.stringify({ kind })
|
|
198
198
|
}));
|
|
199
199
|
return fromWireSnapshotRequest(body.snapshot_request ?? body);
|
|
200
200
|
}
|
|
@@ -315,6 +315,7 @@ function fromWireSnapshotRequest(value) {
|
|
|
315
315
|
return {
|
|
316
316
|
id: requiredString(input.id, "snapshot_request.id"),
|
|
317
317
|
sessionId: requiredString(input.session_id, "snapshot_request.session_id"),
|
|
318
|
+
kind: input.kind === "visible_history_replace" ? "visible_history_replace" : "immutable",
|
|
318
319
|
throughSequence: requiredNumber(input.through_sequence, "snapshot_request.through_sequence"),
|
|
319
320
|
status,
|
|
320
321
|
...createdAt === void 0 ? {} : { createdAt },
|
|
@@ -1398,6 +1399,7 @@ var DshHostConnector = class {
|
|
|
1398
1399
|
#timer;
|
|
1399
1400
|
#heartbeatTimer;
|
|
1400
1401
|
#pollPromise;
|
|
1402
|
+
#localSyncControlPromise;
|
|
1401
1403
|
#heartbeatPromise;
|
|
1402
1404
|
#stopPromise;
|
|
1403
1405
|
#backgroundFatalError;
|
|
@@ -1429,6 +1431,47 @@ var DshHostConnector = class {
|
|
|
1429
1431
|
get stopped() {
|
|
1430
1432
|
return this.#stopped;
|
|
1431
1433
|
}
|
|
1434
|
+
localSyncStatus() {
|
|
1435
|
+
const state = this.#requireState();
|
|
1436
|
+
const pendingOperations = state.outbox.filter((operation) => operation.kind === "local_turn");
|
|
1437
|
+
const pending = pendingOperations.length;
|
|
1438
|
+
const discoverFrom = pendingOperations.reduce((sequence, operation) => Math.max(sequence, operation.dshToSequence), state.publishedDshSequence);
|
|
1439
|
+
const discoverable = state.activeRequest === void 0 ? captureCompletedLocalTurns(this.#host.snapshotFrom(discoverFrom), discoverFrom, this.#config.dshSessionId).turns.length : 0;
|
|
1440
|
+
return {
|
|
1441
|
+
sessionId: this.#config.sessionId,
|
|
1442
|
+
localSessionId: this.#config.dshSessionId,
|
|
1443
|
+
automaticUpload: state.automaticUpload,
|
|
1444
|
+
pendingLocalTurns: pending,
|
|
1445
|
+
uploadableLocalTurns: pending + discoverable
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
async setLocalAutoUpload(enabled) {
|
|
1449
|
+
const status = await this.#withLocalSyncControl(async () => {
|
|
1450
|
+
const state = this.#requireState();
|
|
1451
|
+
state.automaticUpload = enabled;
|
|
1452
|
+
await this.#stateStore.save(state);
|
|
1453
|
+
return this.localSyncStatus();
|
|
1454
|
+
});
|
|
1455
|
+
if (enabled)
|
|
1456
|
+
await this.pollOnce();
|
|
1457
|
+
return enabled ? this.localSyncStatus() : status;
|
|
1458
|
+
}
|
|
1459
|
+
uploadLocalTurns() {
|
|
1460
|
+
return this.#withLocalSyncControl(async () => {
|
|
1461
|
+
const before = this.localSyncStatus();
|
|
1462
|
+
await this.#flushOutbox(true);
|
|
1463
|
+
await this.#captureLocalTurns(0);
|
|
1464
|
+
const discoveredLocalTurns = this.localSyncStatus().pendingLocalTurns;
|
|
1465
|
+
const pendingBefore = this.localSyncStatus().pendingLocalTurns;
|
|
1466
|
+
await this.#flushOutbox(true);
|
|
1467
|
+
const after = this.localSyncStatus();
|
|
1468
|
+
return {
|
|
1469
|
+
...after,
|
|
1470
|
+
discoveredLocalTurns,
|
|
1471
|
+
uploadedLocalTurns: Math.max(0, before.pendingLocalTurns + pendingBefore - after.pendingLocalTurns)
|
|
1472
|
+
};
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1432
1475
|
async start(options = {}) {
|
|
1433
1476
|
if (this.#started)
|
|
1434
1477
|
throw new Error("DSH connector is already started");
|
|
@@ -1493,7 +1536,7 @@ var DshHostConnector = class {
|
|
|
1493
1536
|
this.#notifyLifecycle("idle");
|
|
1494
1537
|
if (options.schedule !== false)
|
|
1495
1538
|
this.#scheduleHeartbeat();
|
|
1496
|
-
await this.#flushOutbox();
|
|
1539
|
+
await this.#flushOutbox(this.#requireState().automaticUpload);
|
|
1497
1540
|
await this.#finalizeDeliveredRequest();
|
|
1498
1541
|
if (this.#state.activeRequest !== void 0) {
|
|
1499
1542
|
await this.#withExecutionPermit(() => this.#recoverActiveRequest());
|
|
@@ -1514,6 +1557,10 @@ var DshHostConnector = class {
|
|
|
1514
1557
|
if (!this.#started || this.#stopped) {
|
|
1515
1558
|
return Promise.reject(new Error("DSH connector is not running"));
|
|
1516
1559
|
}
|
|
1560
|
+
const localSyncControl = this.#localSyncControlPromise;
|
|
1561
|
+
if (localSyncControl !== void 0) {
|
|
1562
|
+
return localSyncControl.then(() => this.pollOnce());
|
|
1563
|
+
}
|
|
1517
1564
|
if (this.#pollPromise !== void 0)
|
|
1518
1565
|
return this.#pollPromise;
|
|
1519
1566
|
this.#pollPromise = this.#poll().then((result) => {
|
|
@@ -1528,6 +1575,28 @@ var DshHostConnector = class {
|
|
|
1528
1575
|
});
|
|
1529
1576
|
return this.#pollPromise;
|
|
1530
1577
|
}
|
|
1578
|
+
async #withLocalSyncControl(operation) {
|
|
1579
|
+
const previous = this.#localSyncControlPromise;
|
|
1580
|
+
let release;
|
|
1581
|
+
const barrier = new Promise((resolve) => {
|
|
1582
|
+
release = resolve;
|
|
1583
|
+
});
|
|
1584
|
+
const current = (previous ?? Promise.resolve()).then(() => barrier);
|
|
1585
|
+
this.#localSyncControlPromise = current;
|
|
1586
|
+
try {
|
|
1587
|
+
await previous;
|
|
1588
|
+
const poll = this.#pollPromise;
|
|
1589
|
+
if (poll !== void 0)
|
|
1590
|
+
await poll;
|
|
1591
|
+
if (!this.#started || this.#stopped)
|
|
1592
|
+
throw new Error("DSH connector is not running");
|
|
1593
|
+
return await operation();
|
|
1594
|
+
} finally {
|
|
1595
|
+
release();
|
|
1596
|
+
if (this.#localSyncControlPromise === current)
|
|
1597
|
+
this.#localSyncControlPromise = void 0;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1531
1600
|
stop() {
|
|
1532
1601
|
this.#stopPromise ??= (async () => {
|
|
1533
1602
|
this.#stopped = true;
|
|
@@ -1557,14 +1626,17 @@ var DshHostConnector = class {
|
|
|
1557
1626
|
}
|
|
1558
1627
|
async #poll() {
|
|
1559
1628
|
const state = this.#requireState();
|
|
1560
|
-
await this.#flushOutbox();
|
|
1629
|
+
await this.#flushOutbox(state.automaticUpload);
|
|
1561
1630
|
await this.#finalizeDeliveredRequest();
|
|
1562
1631
|
if (state.activeRequest !== void 0) {
|
|
1563
1632
|
await this.#withExecutionPermit(() => this.#recoverActiveRequest());
|
|
1564
1633
|
return { scanned: 0, claimed: 0, completed: 1 };
|
|
1565
1634
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1635
|
+
if (state.automaticUpload) {
|
|
1636
|
+
await this.#captureLocalTurns();
|
|
1637
|
+
await this.#flushOutbox(true);
|
|
1638
|
+
}
|
|
1639
|
+
const manualUploadRequired = !state.automaticUpload && this.localSyncStatus().uploadableLocalTurns > 0;
|
|
1568
1640
|
let scanCursor = state.projectionCursor;
|
|
1569
1641
|
let scanned = 0;
|
|
1570
1642
|
while (!this.#stopped) {
|
|
@@ -1580,6 +1652,9 @@ var DshHostConnector = class {
|
|
|
1580
1652
|
continue;
|
|
1581
1653
|
}
|
|
1582
1654
|
await this.#projectCanonicalBatch(passive, passive.at(-1)?.sequence ?? state.projectionCursor);
|
|
1655
|
+
if (manualUploadRequired) {
|
|
1656
|
+
return { scanned, claimed: 0, completed: 0 };
|
|
1657
|
+
}
|
|
1583
1658
|
if (profile.provider !== void 0 && profile.provider !== this.#config.provider) {
|
|
1584
1659
|
throw new Error("DeepSeek Harness Agent request provider does not match the configured Host binding");
|
|
1585
1660
|
}
|
|
@@ -1757,7 +1832,7 @@ var DshHostConnector = class {
|
|
|
1757
1832
|
state.serverCursor = Math.max(state.serverCursor, throughSequence);
|
|
1758
1833
|
await this.#stateStore.save(state);
|
|
1759
1834
|
}
|
|
1760
|
-
async #captureLocalTurns() {
|
|
1835
|
+
async #captureLocalTurns(basedOnSequence) {
|
|
1761
1836
|
const state = this.#requireState();
|
|
1762
1837
|
if (state.activeRequest !== void 0 || state.outbox.length > 0)
|
|
1763
1838
|
return;
|
|
@@ -1775,7 +1850,7 @@ var DshHostConnector = class {
|
|
|
1775
1850
|
input: {
|
|
1776
1851
|
localTurnId: turn.localTurnId,
|
|
1777
1852
|
runtimeId: runtime.id,
|
|
1778
|
-
basedOnSequence: state.serverCursor,
|
|
1853
|
+
basedOnSequence: basedOnSequence ?? state.serverCursor,
|
|
1779
1854
|
occurredAt: turn.occurredAt,
|
|
1780
1855
|
observedModel: this.#config.model,
|
|
1781
1856
|
requestPayload: {
|
|
@@ -1877,13 +1952,15 @@ var DshHostConnector = class {
|
|
|
1877
1952
|
});
|
|
1878
1953
|
return operations;
|
|
1879
1954
|
}
|
|
1880
|
-
async #flushOutbox() {
|
|
1955
|
+
async #flushOutbox(includeLocalTurns = true) {
|
|
1881
1956
|
const state = this.#requireState();
|
|
1882
1957
|
const runtime = this.#requireRuntime();
|
|
1883
1958
|
while (state.outbox.length > 0) {
|
|
1884
1959
|
const operation = state.outbox[0];
|
|
1885
1960
|
if (operation === void 0)
|
|
1886
1961
|
break;
|
|
1962
|
+
if (operation.kind === "local_turn" && !includeLocalTurns)
|
|
1963
|
+
break;
|
|
1887
1964
|
if (operation.input.runtimeId !== runtime.id) {
|
|
1888
1965
|
throw new Error("DSH connector outbox belongs to a different runtime");
|
|
1889
1966
|
}
|
|
@@ -1965,7 +2042,7 @@ var DshHostConnector = class {
|
|
|
1965
2042
|
throw new Error("Persisted DSH Session exists without connector state; refusing ambiguous resume");
|
|
1966
2043
|
}
|
|
1967
2044
|
return {
|
|
1968
|
-
version:
|
|
2045
|
+
version: 3,
|
|
1969
2046
|
binding: {
|
|
1970
2047
|
projectId: this.#config.projectId,
|
|
1971
2048
|
sessionId: this.#config.sessionId,
|
|
@@ -1974,6 +2051,7 @@ var DshHostConnector = class {
|
|
|
1974
2051
|
serverCursor: 0,
|
|
1975
2052
|
projectionCursor: 0,
|
|
1976
2053
|
publishedDshSequence: 0,
|
|
2054
|
+
automaticUpload: true,
|
|
1977
2055
|
outbox: []
|
|
1978
2056
|
};
|
|
1979
2057
|
}
|
|
@@ -3273,6 +3351,21 @@ var DshProjectManager = class {
|
|
|
3273
3351
|
activeSessionIds() {
|
|
3274
3352
|
return [...this.#managed].filter(([, binding]) => binding.phase === "active" && !binding.connector.stopped).map(([sessionId]) => sessionId).sort();
|
|
3275
3353
|
}
|
|
3354
|
+
localSyncStatuses() {
|
|
3355
|
+
return [...this.#managed.entries()].filter(([, binding]) => binding.phase === "active" && !binding.connector.stopped).flatMap(([, binding]) => binding.connector.localSyncStatus?.() ?? []).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
|
|
3356
|
+
}
|
|
3357
|
+
async setLocalAutoUpload(sessionId, enabled) {
|
|
3358
|
+
const connector = this.#requireManagedConnector(sessionId);
|
|
3359
|
+
if (!connector.setLocalAutoUpload)
|
|
3360
|
+
throw new Error("This DSH conversation does not support upload controls");
|
|
3361
|
+
return connector.setLocalAutoUpload(enabled);
|
|
3362
|
+
}
|
|
3363
|
+
async uploadLocalTurns(sessionId) {
|
|
3364
|
+
const connector = this.#requireManagedConnector(sessionId);
|
|
3365
|
+
if (!connector.uploadLocalTurns)
|
|
3366
|
+
throw new Error("This DSH conversation does not support manual upload");
|
|
3367
|
+
return connector.uploadLocalTurns();
|
|
3368
|
+
}
|
|
3276
3369
|
async start() {
|
|
3277
3370
|
if (this.#started)
|
|
3278
3371
|
throw new Error("DSH project manager is already started");
|
|
@@ -3512,6 +3605,13 @@ var DshProjectManager = class {
|
|
|
3512
3605
|
throw new Error("Authenticated GatherThread actor is unavailable");
|
|
3513
3606
|
return this.#actor;
|
|
3514
3607
|
}
|
|
3608
|
+
#requireManagedConnector(sessionId) {
|
|
3609
|
+
const binding = this.#managed.get(sessionId);
|
|
3610
|
+
if (!binding || binding.phase !== "active" || binding.connector.stopped) {
|
|
3611
|
+
throw new Error("The selected DSH conversation is not active");
|
|
3612
|
+
}
|
|
3613
|
+
return binding.connector;
|
|
3614
|
+
}
|
|
3515
3615
|
#reportError(error) {
|
|
3516
3616
|
this.#onBackgroundError?.(error);
|
|
3517
3617
|
}
|
|
@@ -3660,17 +3760,20 @@ var FileConnectorStateStore = class {
|
|
|
3660
3760
|
function validateConnectorState(value) {
|
|
3661
3761
|
const state = requiredObject4(value, "state");
|
|
3662
3762
|
const legacy = state.version === 1;
|
|
3763
|
+
const legacyUploadPolicy = state.version === 1 || state.version === 2;
|
|
3663
3764
|
exactKeys(state, /* @__PURE__ */ new Set([
|
|
3664
3765
|
"version",
|
|
3665
3766
|
"binding",
|
|
3666
3767
|
"serverCursor",
|
|
3667
3768
|
...legacy ? [] : ["projectionCursor"],
|
|
3668
3769
|
"publishedDshSequence",
|
|
3770
|
+
...legacyUploadPolicy ? [] : ["automaticUpload"],
|
|
3669
3771
|
"activeRequest",
|
|
3670
3772
|
"outbox"
|
|
3671
3773
|
]), "state");
|
|
3672
|
-
if (
|
|
3774
|
+
if (state.version !== 1 && state.version !== 2 && state.version !== 3) {
|
|
3673
3775
|
throw new Error("Unsupported DSH connector state version");
|
|
3776
|
+
}
|
|
3674
3777
|
const binding = requiredObject4(state.binding, "state.binding");
|
|
3675
3778
|
exactKeys(binding, /* @__PURE__ */ new Set(["projectId", "sessionId", "dshSessionId"]), "state.binding");
|
|
3676
3779
|
const serverCursor = nonNegativeInteger(state.serverCursor, "state.serverCursor");
|
|
@@ -3681,7 +3784,7 @@ function validateConnectorState(value) {
|
|
|
3681
3784
|
throw new Error("state.outbox must be an array");
|
|
3682
3785
|
const outbox = state.outbox.map(parseOutboxOperation);
|
|
3683
3786
|
const validated = {
|
|
3684
|
-
version:
|
|
3787
|
+
version: 3,
|
|
3685
3788
|
binding: {
|
|
3686
3789
|
projectId: safeString(binding.projectId, "state.binding.projectId", 128),
|
|
3687
3790
|
sessionId: safeString(binding.sessionId, "state.binding.sessionId", 128),
|
|
@@ -3690,6 +3793,7 @@ function validateConnectorState(value) {
|
|
|
3690
3793
|
serverCursor,
|
|
3691
3794
|
projectionCursor,
|
|
3692
3795
|
publishedDshSequence,
|
|
3796
|
+
automaticUpload: legacyUploadPolicy ? true : requiredBoolean(state.automaticUpload, "state.automaticUpload"),
|
|
3693
3797
|
...activeRequest === void 0 ? {} : { activeRequest },
|
|
3694
3798
|
outbox
|
|
3695
3799
|
};
|
|
@@ -3869,6 +3973,11 @@ function nonNegativeInteger(value, label) {
|
|
|
3869
3973
|
}
|
|
3870
3974
|
return Number(value);
|
|
3871
3975
|
}
|
|
3976
|
+
function requiredBoolean(value, label) {
|
|
3977
|
+
if (typeof value !== "boolean")
|
|
3978
|
+
throw new Error(`${label} must be a boolean`);
|
|
3979
|
+
return value;
|
|
3980
|
+
}
|
|
3872
3981
|
function positiveInteger(value, label) {
|
|
3873
3982
|
const parsed = nonNegativeInteger(value, label);
|
|
3874
3983
|
if (parsed === 0)
|
|
@@ -4133,7 +4242,9 @@ var RPC_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
|
4133
4242
|
"pairing/cancel",
|
|
4134
4243
|
"catalog/get",
|
|
4135
4244
|
"connection/configure",
|
|
4136
|
-
"connection/disconnect"
|
|
4245
|
+
"connection/disconnect",
|
|
4246
|
+
"sync/set-auto-upload",
|
|
4247
|
+
"sync/upload"
|
|
4137
4248
|
]);
|
|
4138
4249
|
var MAX_CATALOG_PROVIDERS = 64;
|
|
4139
4250
|
var MAX_CATALOG_MODELS = 256;
|
|
@@ -4202,7 +4313,8 @@ var DshNativeHostController = class {
|
|
|
4202
4313
|
...this.#grant.route === void 0 ? {} : {
|
|
4203
4314
|
route: { ...this.#grant.route },
|
|
4204
4315
|
projectCount: this.#projects.size,
|
|
4205
|
-
bindings: this.#currentBindings(this.#grant.route).slice(0, 100)
|
|
4316
|
+
bindings: this.#currentBindings(this.#grant.route).slice(0, 100),
|
|
4317
|
+
localSync: this.#currentLocalSync().slice(0, 100)
|
|
4206
4318
|
}
|
|
4207
4319
|
},
|
|
4208
4320
|
...this.#pairing === void 0 ? {} : {
|
|
@@ -4371,6 +4483,34 @@ var DshNativeHostController = class {
|
|
|
4371
4483
|
this.#options.status.setConnection("stopped");
|
|
4372
4484
|
return this.publicState();
|
|
4373
4485
|
}
|
|
4486
|
+
async setLocalAutoUpload(inputValue) {
|
|
4487
|
+
this.#assertAvailable();
|
|
4488
|
+
const input = exactInput(inputValue, /* @__PURE__ */ new Set(["projectId", "sessionId", "enabled"]));
|
|
4489
|
+
const projectId = safeInputIdentifier(input.projectId, "projectId");
|
|
4490
|
+
const sessionId = safeInputIdentifier(input.sessionId, "sessionId");
|
|
4491
|
+
if (typeof input.enabled !== "boolean")
|
|
4492
|
+
throw new Error("enabled must be true or false");
|
|
4493
|
+
const owner = this.#owners.get(projectId);
|
|
4494
|
+
if (!owner)
|
|
4495
|
+
throw new Error("The selected GatherThread Project is not active");
|
|
4496
|
+
if (!owner.setLocalAutoUpload)
|
|
4497
|
+
throw new Error("This DSH Project does not expose upload controls");
|
|
4498
|
+
await owner.setLocalAutoUpload(sessionId, input.enabled);
|
|
4499
|
+
return this.publicState();
|
|
4500
|
+
}
|
|
4501
|
+
async uploadLocalTurns(inputValue) {
|
|
4502
|
+
this.#assertAvailable();
|
|
4503
|
+
const input = exactInput(inputValue, /* @__PURE__ */ new Set(["projectId", "sessionId"]));
|
|
4504
|
+
const projectId = safeInputIdentifier(input.projectId, "projectId");
|
|
4505
|
+
const sessionId = safeInputIdentifier(input.sessionId, "sessionId");
|
|
4506
|
+
const owner = this.#owners.get(projectId);
|
|
4507
|
+
if (!owner)
|
|
4508
|
+
throw new Error("The selected GatherThread Project is not active");
|
|
4509
|
+
if (!owner.uploadLocalTurns)
|
|
4510
|
+
throw new Error("This DSH Project does not expose manual upload");
|
|
4511
|
+
await owner.uploadLocalTurns(sessionId);
|
|
4512
|
+
return this.publicState();
|
|
4513
|
+
}
|
|
4374
4514
|
dispose() {
|
|
4375
4515
|
this.#disposePromise ??= (async () => {
|
|
4376
4516
|
this.#disposed = true;
|
|
@@ -4507,6 +4647,20 @@ var DshNativeHostController = class {
|
|
|
4507
4647
|
model: route.model
|
|
4508
4648
|
})).sort((left, right) => left.projectName.localeCompare(right.projectName) || left.projectId.localeCompare(right.projectId));
|
|
4509
4649
|
}
|
|
4650
|
+
#currentLocalSync() {
|
|
4651
|
+
const titles = new Map(this.#options.status.snapshot().sessions.map((session) => [session.sessionId, session.title]));
|
|
4652
|
+
return [...this.#owners.entries()].flatMap(([projectId, owner]) => {
|
|
4653
|
+
const project = this.#projects.get(projectId);
|
|
4654
|
+
if (!project)
|
|
4655
|
+
return [];
|
|
4656
|
+
return (owner.localSyncStatuses?.() ?? []).map((sync) => ({
|
|
4657
|
+
...sync,
|
|
4658
|
+
projectId,
|
|
4659
|
+
projectName: boundedPublicText2(project.name, "project name", 160),
|
|
4660
|
+
title: titles.get(sync.sessionId) ?? sync.sessionId
|
|
4661
|
+
}));
|
|
4662
|
+
}).sort((left, right) => left.projectName.localeCompare(right.projectName) || left.title.localeCompare(right.title) || left.sessionId.localeCompare(right.sessionId));
|
|
4663
|
+
}
|
|
4510
4664
|
#updateProjectStatus(projectId, update) {
|
|
4511
4665
|
if (!this.#projects.has(projectId) || this.#disposed)
|
|
4512
4666
|
return;
|
|
@@ -4628,6 +4782,10 @@ function registerNativeDshRpc(contextValue, controller) {
|
|
|
4628
4782
|
case "connection/disconnect":
|
|
4629
4783
|
exactInput(payload, /* @__PURE__ */ new Set());
|
|
4630
4784
|
return rpcSuccess(await controller.disconnect());
|
|
4785
|
+
case "sync/set-auto-upload":
|
|
4786
|
+
return rpcSuccess(await controller.setLocalAutoUpload(payload));
|
|
4787
|
+
case "sync/upload":
|
|
4788
|
+
return rpcSuccess(await controller.uploadLocalTurns(payload));
|
|
4631
4789
|
default:
|
|
4632
4790
|
return rpcFailure("gatherthread/not-found", "GatherThread DSH action is unavailable");
|
|
4633
4791
|
}
|
|
@@ -4709,6 +4867,9 @@ async function createProductionOwner(options) {
|
|
|
4709
4867
|
}
|
|
4710
4868
|
let stopPromise;
|
|
4711
4869
|
return {
|
|
4870
|
+
localSyncStatuses: () => manager.localSyncStatuses(),
|
|
4871
|
+
setLocalAutoUpload: (sessionId, enabled) => manager.setLocalAutoUpload(sessionId, enabled),
|
|
4872
|
+
uploadLocalTurns: (sessionId) => manager.uploadLocalTurns(sessionId),
|
|
4712
4873
|
stop() {
|
|
4713
4874
|
stopPromise ??= (async () => {
|
|
4714
4875
|
options.signal.removeEventListener("abort", abortFromRoot);
|
|
@@ -4773,6 +4934,9 @@ function createNativeManagedConnector(options) {
|
|
|
4773
4934
|
async start() {
|
|
4774
4935
|
await connector.start();
|
|
4775
4936
|
},
|
|
4937
|
+
localSyncStatus: () => connector.localSyncStatus(),
|
|
4938
|
+
setLocalAutoUpload: (enabled) => connector.setLocalAutoUpload(enabled),
|
|
4939
|
+
uploadLocalTurns: () => connector.uploadLocalTurns(),
|
|
4776
4940
|
stop() {
|
|
4777
4941
|
stopPromise ??= (async () => {
|
|
4778
4942
|
options.rootSignal.removeEventListener("abort", abortFromRoot);
|
package/dist/bundle/plugin.js
CHANGED
|
@@ -512,10 +512,10 @@ var HttpCollaborationClient = class {
|
|
|
512
512
|
const body = requiredObject2(await this.#request(`/snapshot-requests/${encodeURIComponent(requestId)}`));
|
|
513
513
|
return fromWireSnapshotRequest(body.snapshot_request ?? body);
|
|
514
514
|
}
|
|
515
|
-
async createSnapshotRequest(sessionId) {
|
|
515
|
+
async createSnapshotRequest(sessionId, kind = "immutable") {
|
|
516
516
|
const body = requiredObject2(await this.#request(`/sessions/${encodeURIComponent(sessionId)}/snapshot-requests`, {
|
|
517
517
|
method: "POST",
|
|
518
|
-
body:
|
|
518
|
+
body: JSON.stringify({ kind })
|
|
519
519
|
}));
|
|
520
520
|
return fromWireSnapshotRequest(body.snapshot_request ?? body);
|
|
521
521
|
}
|
|
@@ -636,6 +636,7 @@ function fromWireSnapshotRequest(value) {
|
|
|
636
636
|
return {
|
|
637
637
|
id: requiredString2(input.id, "snapshot_request.id"),
|
|
638
638
|
sessionId: requiredString2(input.session_id, "snapshot_request.session_id"),
|
|
639
|
+
kind: input.kind === "visible_history_replace" ? "visible_history_replace" : "immutable",
|
|
639
640
|
throughSequence: requiredNumber(input.through_sequence, "snapshot_request.through_sequence"),
|
|
640
641
|
status,
|
|
641
642
|
...createdAt === void 0 ? {} : { createdAt },
|
|
@@ -1309,6 +1310,7 @@ var DshHostConnector = class {
|
|
|
1309
1310
|
#timer;
|
|
1310
1311
|
#heartbeatTimer;
|
|
1311
1312
|
#pollPromise;
|
|
1313
|
+
#localSyncControlPromise;
|
|
1312
1314
|
#heartbeatPromise;
|
|
1313
1315
|
#stopPromise;
|
|
1314
1316
|
#backgroundFatalError;
|
|
@@ -1340,6 +1342,47 @@ var DshHostConnector = class {
|
|
|
1340
1342
|
get stopped() {
|
|
1341
1343
|
return this.#stopped;
|
|
1342
1344
|
}
|
|
1345
|
+
localSyncStatus() {
|
|
1346
|
+
const state = this.#requireState();
|
|
1347
|
+
const pendingOperations = state.outbox.filter((operation) => operation.kind === "local_turn");
|
|
1348
|
+
const pending = pendingOperations.length;
|
|
1349
|
+
const discoverFrom = pendingOperations.reduce((sequence, operation) => Math.max(sequence, operation.dshToSequence), state.publishedDshSequence);
|
|
1350
|
+
const discoverable = state.activeRequest === void 0 ? captureCompletedLocalTurns(this.#host.snapshotFrom(discoverFrom), discoverFrom, this.#config.dshSessionId).turns.length : 0;
|
|
1351
|
+
return {
|
|
1352
|
+
sessionId: this.#config.sessionId,
|
|
1353
|
+
localSessionId: this.#config.dshSessionId,
|
|
1354
|
+
automaticUpload: state.automaticUpload,
|
|
1355
|
+
pendingLocalTurns: pending,
|
|
1356
|
+
uploadableLocalTurns: pending + discoverable
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
async setLocalAutoUpload(enabled) {
|
|
1360
|
+
const status = await this.#withLocalSyncControl(async () => {
|
|
1361
|
+
const state = this.#requireState();
|
|
1362
|
+
state.automaticUpload = enabled;
|
|
1363
|
+
await this.#stateStore.save(state);
|
|
1364
|
+
return this.localSyncStatus();
|
|
1365
|
+
});
|
|
1366
|
+
if (enabled)
|
|
1367
|
+
await this.pollOnce();
|
|
1368
|
+
return enabled ? this.localSyncStatus() : status;
|
|
1369
|
+
}
|
|
1370
|
+
uploadLocalTurns() {
|
|
1371
|
+
return this.#withLocalSyncControl(async () => {
|
|
1372
|
+
const before = this.localSyncStatus();
|
|
1373
|
+
await this.#flushOutbox(true);
|
|
1374
|
+
await this.#captureLocalTurns(0);
|
|
1375
|
+
const discoveredLocalTurns = this.localSyncStatus().pendingLocalTurns;
|
|
1376
|
+
const pendingBefore = this.localSyncStatus().pendingLocalTurns;
|
|
1377
|
+
await this.#flushOutbox(true);
|
|
1378
|
+
const after = this.localSyncStatus();
|
|
1379
|
+
return {
|
|
1380
|
+
...after,
|
|
1381
|
+
discoveredLocalTurns,
|
|
1382
|
+
uploadedLocalTurns: Math.max(0, before.pendingLocalTurns + pendingBefore - after.pendingLocalTurns)
|
|
1383
|
+
};
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1343
1386
|
async start(options = {}) {
|
|
1344
1387
|
if (this.#started)
|
|
1345
1388
|
throw new Error("DSH connector is already started");
|
|
@@ -1404,7 +1447,7 @@ var DshHostConnector = class {
|
|
|
1404
1447
|
this.#notifyLifecycle("idle");
|
|
1405
1448
|
if (options.schedule !== false)
|
|
1406
1449
|
this.#scheduleHeartbeat();
|
|
1407
|
-
await this.#flushOutbox();
|
|
1450
|
+
await this.#flushOutbox(this.#requireState().automaticUpload);
|
|
1408
1451
|
await this.#finalizeDeliveredRequest();
|
|
1409
1452
|
if (this.#state.activeRequest !== void 0) {
|
|
1410
1453
|
await this.#withExecutionPermit(() => this.#recoverActiveRequest());
|
|
@@ -1425,6 +1468,10 @@ var DshHostConnector = class {
|
|
|
1425
1468
|
if (!this.#started || this.#stopped) {
|
|
1426
1469
|
return Promise.reject(new Error("DSH connector is not running"));
|
|
1427
1470
|
}
|
|
1471
|
+
const localSyncControl = this.#localSyncControlPromise;
|
|
1472
|
+
if (localSyncControl !== void 0) {
|
|
1473
|
+
return localSyncControl.then(() => this.pollOnce());
|
|
1474
|
+
}
|
|
1428
1475
|
if (this.#pollPromise !== void 0)
|
|
1429
1476
|
return this.#pollPromise;
|
|
1430
1477
|
this.#pollPromise = this.#poll().then((result) => {
|
|
@@ -1439,6 +1486,28 @@ var DshHostConnector = class {
|
|
|
1439
1486
|
});
|
|
1440
1487
|
return this.#pollPromise;
|
|
1441
1488
|
}
|
|
1489
|
+
async #withLocalSyncControl(operation) {
|
|
1490
|
+
const previous = this.#localSyncControlPromise;
|
|
1491
|
+
let release;
|
|
1492
|
+
const barrier = new Promise((resolve) => {
|
|
1493
|
+
release = resolve;
|
|
1494
|
+
});
|
|
1495
|
+
const current = (previous ?? Promise.resolve()).then(() => barrier);
|
|
1496
|
+
this.#localSyncControlPromise = current;
|
|
1497
|
+
try {
|
|
1498
|
+
await previous;
|
|
1499
|
+
const poll = this.#pollPromise;
|
|
1500
|
+
if (poll !== void 0)
|
|
1501
|
+
await poll;
|
|
1502
|
+
if (!this.#started || this.#stopped)
|
|
1503
|
+
throw new Error("DSH connector is not running");
|
|
1504
|
+
return await operation();
|
|
1505
|
+
} finally {
|
|
1506
|
+
release();
|
|
1507
|
+
if (this.#localSyncControlPromise === current)
|
|
1508
|
+
this.#localSyncControlPromise = void 0;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1442
1511
|
stop() {
|
|
1443
1512
|
this.#stopPromise ??= (async () => {
|
|
1444
1513
|
this.#stopped = true;
|
|
@@ -1468,14 +1537,17 @@ var DshHostConnector = class {
|
|
|
1468
1537
|
}
|
|
1469
1538
|
async #poll() {
|
|
1470
1539
|
const state = this.#requireState();
|
|
1471
|
-
await this.#flushOutbox();
|
|
1540
|
+
await this.#flushOutbox(state.automaticUpload);
|
|
1472
1541
|
await this.#finalizeDeliveredRequest();
|
|
1473
1542
|
if (state.activeRequest !== void 0) {
|
|
1474
1543
|
await this.#withExecutionPermit(() => this.#recoverActiveRequest());
|
|
1475
1544
|
return { scanned: 0, claimed: 0, completed: 1 };
|
|
1476
1545
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1546
|
+
if (state.automaticUpload) {
|
|
1547
|
+
await this.#captureLocalTurns();
|
|
1548
|
+
await this.#flushOutbox(true);
|
|
1549
|
+
}
|
|
1550
|
+
const manualUploadRequired = !state.automaticUpload && this.localSyncStatus().uploadableLocalTurns > 0;
|
|
1479
1551
|
let scanCursor = state.projectionCursor;
|
|
1480
1552
|
let scanned = 0;
|
|
1481
1553
|
while (!this.#stopped) {
|
|
@@ -1491,6 +1563,9 @@ var DshHostConnector = class {
|
|
|
1491
1563
|
continue;
|
|
1492
1564
|
}
|
|
1493
1565
|
await this.#projectCanonicalBatch(passive, passive.at(-1)?.sequence ?? state.projectionCursor);
|
|
1566
|
+
if (manualUploadRequired) {
|
|
1567
|
+
return { scanned, claimed: 0, completed: 0 };
|
|
1568
|
+
}
|
|
1494
1569
|
if (profile.provider !== void 0 && profile.provider !== this.#config.provider) {
|
|
1495
1570
|
throw new Error("DeepSeek Harness Agent request provider does not match the configured Host binding");
|
|
1496
1571
|
}
|
|
@@ -1668,7 +1743,7 @@ var DshHostConnector = class {
|
|
|
1668
1743
|
state.serverCursor = Math.max(state.serverCursor, throughSequence);
|
|
1669
1744
|
await this.#stateStore.save(state);
|
|
1670
1745
|
}
|
|
1671
|
-
async #captureLocalTurns() {
|
|
1746
|
+
async #captureLocalTurns(basedOnSequence) {
|
|
1672
1747
|
const state = this.#requireState();
|
|
1673
1748
|
if (state.activeRequest !== void 0 || state.outbox.length > 0)
|
|
1674
1749
|
return;
|
|
@@ -1686,7 +1761,7 @@ var DshHostConnector = class {
|
|
|
1686
1761
|
input: {
|
|
1687
1762
|
localTurnId: turn.localTurnId,
|
|
1688
1763
|
runtimeId: runtime.id,
|
|
1689
|
-
basedOnSequence: state.serverCursor,
|
|
1764
|
+
basedOnSequence: basedOnSequence ?? state.serverCursor,
|
|
1690
1765
|
occurredAt: turn.occurredAt,
|
|
1691
1766
|
observedModel: this.#config.model,
|
|
1692
1767
|
requestPayload: {
|
|
@@ -1788,13 +1863,15 @@ var DshHostConnector = class {
|
|
|
1788
1863
|
});
|
|
1789
1864
|
return operations;
|
|
1790
1865
|
}
|
|
1791
|
-
async #flushOutbox() {
|
|
1866
|
+
async #flushOutbox(includeLocalTurns = true) {
|
|
1792
1867
|
const state = this.#requireState();
|
|
1793
1868
|
const runtime = this.#requireRuntime();
|
|
1794
1869
|
while (state.outbox.length > 0) {
|
|
1795
1870
|
const operation = state.outbox[0];
|
|
1796
1871
|
if (operation === void 0)
|
|
1797
1872
|
break;
|
|
1873
|
+
if (operation.kind === "local_turn" && !includeLocalTurns)
|
|
1874
|
+
break;
|
|
1798
1875
|
if (operation.input.runtimeId !== runtime.id) {
|
|
1799
1876
|
throw new Error("DSH connector outbox belongs to a different runtime");
|
|
1800
1877
|
}
|
|
@@ -1876,7 +1953,7 @@ var DshHostConnector = class {
|
|
|
1876
1953
|
throw new Error("Persisted DSH Session exists without connector state; refusing ambiguous resume");
|
|
1877
1954
|
}
|
|
1878
1955
|
return {
|
|
1879
|
-
version:
|
|
1956
|
+
version: 3,
|
|
1880
1957
|
binding: {
|
|
1881
1958
|
projectId: this.#config.projectId,
|
|
1882
1959
|
sessionId: this.#config.sessionId,
|
|
@@ -1885,6 +1962,7 @@ var DshHostConnector = class {
|
|
|
1885
1962
|
serverCursor: 0,
|
|
1886
1963
|
projectionCursor: 0,
|
|
1887
1964
|
publishedDshSequence: 0,
|
|
1965
|
+
automaticUpload: true,
|
|
1888
1966
|
outbox: []
|
|
1889
1967
|
};
|
|
1890
1968
|
}
|
|
@@ -2648,17 +2726,20 @@ var FileConnectorStateStore = class {
|
|
|
2648
2726
|
function validateConnectorState(value) {
|
|
2649
2727
|
const state = requiredObject3(value, "state");
|
|
2650
2728
|
const legacy = state.version === 1;
|
|
2729
|
+
const legacyUploadPolicy = state.version === 1 || state.version === 2;
|
|
2651
2730
|
exactKeys(state, /* @__PURE__ */ new Set([
|
|
2652
2731
|
"version",
|
|
2653
2732
|
"binding",
|
|
2654
2733
|
"serverCursor",
|
|
2655
2734
|
...legacy ? [] : ["projectionCursor"],
|
|
2656
2735
|
"publishedDshSequence",
|
|
2736
|
+
...legacyUploadPolicy ? [] : ["automaticUpload"],
|
|
2657
2737
|
"activeRequest",
|
|
2658
2738
|
"outbox"
|
|
2659
2739
|
]), "state");
|
|
2660
|
-
if (
|
|
2740
|
+
if (state.version !== 1 && state.version !== 2 && state.version !== 3) {
|
|
2661
2741
|
throw new Error("Unsupported DSH connector state version");
|
|
2742
|
+
}
|
|
2662
2743
|
const binding = requiredObject3(state.binding, "state.binding");
|
|
2663
2744
|
exactKeys(binding, /* @__PURE__ */ new Set(["projectId", "sessionId", "dshSessionId"]), "state.binding");
|
|
2664
2745
|
const serverCursor = nonNegativeInteger(state.serverCursor, "state.serverCursor");
|
|
@@ -2669,7 +2750,7 @@ function validateConnectorState(value) {
|
|
|
2669
2750
|
throw new Error("state.outbox must be an array");
|
|
2670
2751
|
const outbox = state.outbox.map(parseOutboxOperation);
|
|
2671
2752
|
const validated = {
|
|
2672
|
-
version:
|
|
2753
|
+
version: 3,
|
|
2673
2754
|
binding: {
|
|
2674
2755
|
projectId: safeString(binding.projectId, "state.binding.projectId", 128),
|
|
2675
2756
|
sessionId: safeString(binding.sessionId, "state.binding.sessionId", 128),
|
|
@@ -2678,6 +2759,7 @@ function validateConnectorState(value) {
|
|
|
2678
2759
|
serverCursor,
|
|
2679
2760
|
projectionCursor,
|
|
2680
2761
|
publishedDshSequence,
|
|
2762
|
+
automaticUpload: legacyUploadPolicy ? true : requiredBoolean(state.automaticUpload, "state.automaticUpload"),
|
|
2681
2763
|
...activeRequest === void 0 ? {} : { activeRequest },
|
|
2682
2764
|
outbox
|
|
2683
2765
|
};
|
|
@@ -2857,6 +2939,11 @@ function nonNegativeInteger(value, label) {
|
|
|
2857
2939
|
}
|
|
2858
2940
|
return Number(value);
|
|
2859
2941
|
}
|
|
2942
|
+
function requiredBoolean(value, label) {
|
|
2943
|
+
if (typeof value !== "boolean")
|
|
2944
|
+
throw new Error(`${label} must be a boolean`);
|
|
2945
|
+
return value;
|
|
2946
|
+
}
|
|
2860
2947
|
function positiveInteger(value, label) {
|
|
2861
2948
|
const parsed = nonNegativeInteger(value, label);
|
|
2862
2949
|
if (parsed === 0)
|
|
@@ -3020,6 +3107,21 @@ var DshProjectManager = class {
|
|
|
3020
3107
|
activeSessionIds() {
|
|
3021
3108
|
return [...this.#managed].filter(([, binding]) => binding.phase === "active" && !binding.connector.stopped).map(([sessionId]) => sessionId).sort();
|
|
3022
3109
|
}
|
|
3110
|
+
localSyncStatuses() {
|
|
3111
|
+
return [...this.#managed.entries()].filter(([, binding]) => binding.phase === "active" && !binding.connector.stopped).flatMap(([, binding]) => binding.connector.localSyncStatus?.() ?? []).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
|
|
3112
|
+
}
|
|
3113
|
+
async setLocalAutoUpload(sessionId, enabled) {
|
|
3114
|
+
const connector = this.#requireManagedConnector(sessionId);
|
|
3115
|
+
if (!connector.setLocalAutoUpload)
|
|
3116
|
+
throw new Error("This DSH conversation does not support upload controls");
|
|
3117
|
+
return connector.setLocalAutoUpload(enabled);
|
|
3118
|
+
}
|
|
3119
|
+
async uploadLocalTurns(sessionId) {
|
|
3120
|
+
const connector = this.#requireManagedConnector(sessionId);
|
|
3121
|
+
if (!connector.uploadLocalTurns)
|
|
3122
|
+
throw new Error("This DSH conversation does not support manual upload");
|
|
3123
|
+
return connector.uploadLocalTurns();
|
|
3124
|
+
}
|
|
3023
3125
|
async start() {
|
|
3024
3126
|
if (this.#started)
|
|
3025
3127
|
throw new Error("DSH project manager is already started");
|
|
@@ -3259,6 +3361,13 @@ var DshProjectManager = class {
|
|
|
3259
3361
|
throw new Error("Authenticated GatherThread actor is unavailable");
|
|
3260
3362
|
return this.#actor;
|
|
3261
3363
|
}
|
|
3364
|
+
#requireManagedConnector(sessionId) {
|
|
3365
|
+
const binding = this.#managed.get(sessionId);
|
|
3366
|
+
if (!binding || binding.phase !== "active" || binding.connector.stopped) {
|
|
3367
|
+
throw new Error("The selected DSH conversation is not active");
|
|
3368
|
+
}
|
|
3369
|
+
return binding.connector;
|
|
3370
|
+
}
|
|
3262
3371
|
#reportError(error) {
|
|
3263
3372
|
this.#onBackgroundError?.(error);
|
|
3264
3373
|
}
|