dsh-mobile 0.8.6 → 0.8.8
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/CHANGELOG.md +27 -2
- package/CONTRIBUTORS.md +12 -0
- package/README.en.md +20 -14
- package/README.md +17 -13
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/apps/mobile/README.md +21 -7
- package/apps/mobile/README.zh-CN.md +21 -7
- package/apps/mobile/release.json +3 -3
- package/assets/brand/plugin-icon.png +0 -0
- package/docs/ATTACH_EXISTING_FRPS.en.md +2 -2
- package/docs/CPOLAR_SUBDOMAIN.en.md +1 -1
- package/docs/CPOLAR_SUBDOMAIN.md +1 -1
- package/docs/IROH.en.md +5 -5
- package/docs/IROH.md +6 -6
- package/docs/MOBILE_COMPOSER_TOOLBAR.en.md +2 -2
- package/docs/MOBILE_COMPOSER_TOOLBAR.md +2 -2
- package/docs/MOBILE_PLUGIN_ADAPTATION.en.md +9 -0
- package/docs/MOBILE_PLUGIN_ADAPTATION.md +9 -0
- package/docs/MOBILE_PLUGIN_BASELINE.en.md +1 -1
- package/docs/MOBILE_PLUGIN_BASELINE.md +1 -1
- package/docs/WEB_PUSH.en.md +2 -2
- package/docs/WEB_PUSH.md +2 -2
- package/index.mjs +6 -0
- package/lib/client.js +359 -12
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +25 -3
- package/lib/index.mjs +319 -107
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +489 -3
- package/lib/mobile-layout.js.map +1 -1
- package/native/iroh-host/README.md +10 -4
- package/package.json +9 -4
- package/packages/question-fixes/package.json +1 -1
package/lib/index.d.mts
CHANGED
|
@@ -1351,10 +1351,21 @@ export declare class MobileAccessGateway {
|
|
|
1351
1351
|
private proxyHttpOnce;
|
|
1352
1352
|
private abortSessionResources;
|
|
1353
1353
|
private broadcastExtensionChange;
|
|
1354
|
-
/**
|
|
1354
|
+
/**
|
|
1355
|
+
* Fan a completed task to every phone holding this gateway's event stream.
|
|
1356
|
+
*
|
|
1357
|
+
* `sessionId` and `turn` always stay; `title`/`body` are optional and are
|
|
1358
|
+
* emitted together so an older App ignores them and falls back to its own
|
|
1359
|
+
* localized text. This layer re-bounds the strings (code-point safe) instead
|
|
1360
|
+
* of trusting the caller; redaction is deliberately owned only by the
|
|
1361
|
+
* task-events derivation, not repeated here, because this method is also
|
|
1362
|
+
* callable by fixtures that bypass the watcher.
|
|
1363
|
+
*/
|
|
1355
1364
|
broadcastTaskEvent(event: {
|
|
1356
1365
|
readonly sessionId: string;
|
|
1357
1366
|
readonly turn: number;
|
|
1367
|
+
readonly title?: string;
|
|
1368
|
+
readonly body?: string;
|
|
1358
1369
|
}): void;
|
|
1359
1370
|
/** Notify only the device whose persistent credential was revoked by the Host. */
|
|
1360
1371
|
private broadcastDeviceRevoked;
|
|
@@ -2761,13 +2772,22 @@ export declare function irohGatewayConfig(template: ResolvedGatewayConfig, endpo
|
|
|
2761
2772
|
* The exact moment a run ends is known only on the Host (the phone page can
|
|
2762
2773
|
* merely infer it from UI state), so this module watches the public
|
|
2763
2774
|
* `session/event` bus for completed root turns and hands them to a hub that
|
|
2764
|
-
* fans out to every live mobile gateway.
|
|
2765
|
-
*
|
|
2775
|
+
* fans out to every live mobile gateway.
|
|
2776
|
+
*
|
|
2777
|
+
* Phones still render the final notification locally, but the event carries an
|
|
2778
|
+
* optional, already-bounded title/body derived from the Session title and the
|
|
2779
|
+
* turn's first non-empty assistant line. The Android background service uses
|
|
2780
|
+
* that text when present and falls back to its localized strings when the
|
|
2781
|
+
* fields are absent, so older Apps that ignore the new fields keep working.
|
|
2766
2782
|
*/
|
|
2767
2783
|
/** A completed root turn worth announcing on paired phones. */
|
|
2768
2784
|
interface TaskCompletionEvent {
|
|
2769
2785
|
readonly sessionId: string;
|
|
2770
2786
|
readonly turn: number;
|
|
2787
|
+
/** Bounded Session title; present only with a bounded body. */
|
|
2788
|
+
readonly title?: string;
|
|
2789
|
+
/** Bounded first non-empty assistant line of the turn; present only with a title. */
|
|
2790
|
+
readonly body?: string;
|
|
2771
2791
|
}
|
|
2772
2792
|
/** Minimal session shape read off the `session/event` bus. */
|
|
2773
2793
|
interface TaskEventSession {
|
|
@@ -2793,6 +2813,8 @@ interface TaskEventWatcherOptions {
|
|
|
2793
2813
|
/** Trailing debounce per session that merges turn-boundary bursts. */
|
|
2794
2814
|
readonly debounceMs?: number;
|
|
2795
2815
|
readonly onTaskCompleted: (event: TaskCompletionEvent) => void;
|
|
2816
|
+
/** Read the Session's display title (normally `sessionProjections.stateOf(session, 'title')`). */
|
|
2817
|
+
readonly readSessionTitle?: ((session: TaskEventSession) => unknown) | undefined;
|
|
2796
2818
|
readonly log?: (event: string, fields: Readonly<Record<string, string | number | boolean>>) => void;
|
|
2797
2819
|
}
|
|
2798
2820
|
export declare const TASK_EVENT_DEBOUNCE_MS = 1000;
|
package/lib/index.mjs
CHANGED
|
@@ -1864,6 +1864,200 @@ var MobileAccessGatewayController = class {
|
|
|
1864
1864
|
return run;
|
|
1865
1865
|
}
|
|
1866
1866
|
};
|
|
1867
|
+
// src/task-events.ts
|
|
1868
|
+
const TASK_EVENT_DEBOUNCE_MS = 1e3;
|
|
1869
|
+
/**
|
|
1870
|
+
* Wire bounds for the optional title/body. They mirror the Android bridge
|
|
1871
|
+
* policy constants (`NativeBridgePolicy.TASK_NOTIFICATION_TITLE_CHARS` = 80,
|
|
1872
|
+
* `TASK_NOTIFICATION_BODY_CHARS` = 200) so the App never truncates a plugin
|
|
1873
|
+
* value again with a different unit.
|
|
1874
|
+
*/
|
|
1875
|
+
const TASK_ANNOUNCEMENT_LIMITS = Object.freeze({
|
|
1876
|
+
title: 80,
|
|
1877
|
+
body: 200
|
|
1878
|
+
});
|
|
1879
|
+
/** Bounded number of Sessions whose pending assistant line is retained. */
|
|
1880
|
+
const MAX_PENDING_TEXT = 128;
|
|
1881
|
+
function record(value) {
|
|
1882
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1883
|
+
}
|
|
1884
|
+
function turnNumber(value) {
|
|
1885
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
1886
|
+
}
|
|
1887
|
+
function turnData(value) {
|
|
1888
|
+
const data = record(value);
|
|
1889
|
+
if (data === void 0) return {
|
|
1890
|
+
turn: 0,
|
|
1891
|
+
completed: false
|
|
1892
|
+
};
|
|
1893
|
+
const reason = data.reason;
|
|
1894
|
+
const completed = typeof reason === "string" ? reason === "completed" : reason !== null && typeof reason === "object" && reason.kind === "completed";
|
|
1895
|
+
return {
|
|
1896
|
+
turn: turnNumber(data.turn),
|
|
1897
|
+
completed
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
const CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
|
|
1901
|
+
/**
|
|
1902
|
+
* Any first line matching one of these is discarded whole rather than
|
|
1903
|
+
* partially rewritten. A partial path replacement is how a username or file
|
|
1904
|
+
* name leaks: e.g. a Windows path with a space used to leave the tail behind.
|
|
1905
|
+
* The body comes from a user-visible assistant message, not from logs, but a
|
|
1906
|
+
* model can still quote a path, a URL, a JWT/AWS key or a PEM header.
|
|
1907
|
+
*/
|
|
1908
|
+
const SENSITIVE_TEXT_PATTERNS = [
|
|
1909
|
+
/\b(?:https?|file):\/\/\S+/iu,
|
|
1910
|
+
/[A-Za-z]:[\\/][^\r\n"']*/u,
|
|
1911
|
+
/\\\\[^\s"']+/u,
|
|
1912
|
+
/~[\\/][^\r\n"']*/u,
|
|
1913
|
+
/(?:^|[\s"'(])\/(?:[^\s/"']+\/)+[^\s"']*/u,
|
|
1914
|
+
/(?:^|\s)\/[A-Za-z0-9._-]+(?=\s|$)/u,
|
|
1915
|
+
/\beyJ[\w-]{8,}/u,
|
|
1916
|
+
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/u,
|
|
1917
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/u,
|
|
1918
|
+
/\b(?:password|passphrase|token|secret)\s+(?:is|was)\s+\S+/iu,
|
|
1919
|
+
/\b(?:sk|rk|pk)-[A-Za-z0-9_-]{12,}\b/u,
|
|
1920
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{16,}\b/u,
|
|
1921
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/iu,
|
|
1922
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/iu,
|
|
1923
|
+
/\b(?:api[_-]?key|access[_-]?token|token|secret|password|passwd)\s*[:=]\s*['"]?[^\s'"]{4,}/iu
|
|
1924
|
+
];
|
|
1925
|
+
function boundedText$1(value, limit) {
|
|
1926
|
+
if (typeof value !== "string") return void 0;
|
|
1927
|
+
const normalized = value.normalize("NFC").replace(CONTROL_CHARACTERS, " ").replace(/\s+/gu, " ").trim();
|
|
1928
|
+
if (normalized === "") return void 0;
|
|
1929
|
+
if (SENSITIVE_TEXT_PATTERNS.some((pattern) => pattern.test(normalized))) return void 0;
|
|
1930
|
+
const characters = Array.from(normalized);
|
|
1931
|
+
return characters.length > limit ? characters.slice(0, limit).join("").trim() : normalized;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Build the optional wire text. Both fields are present or both are absent:
|
|
1935
|
+
* a title without a body (or vice versa) would make the App mix plugin text
|
|
1936
|
+
* with its fixed fallback strings.
|
|
1937
|
+
*/
|
|
1938
|
+
function taskAnnouncementText(title, body) {
|
|
1939
|
+
const boundedTitle = boundedText$1(title, TASK_ANNOUNCEMENT_LIMITS.title);
|
|
1940
|
+
const boundedBody = boundedText$1(body, TASK_ANNOUNCEMENT_LIMITS.body);
|
|
1941
|
+
if (boundedTitle === void 0 || boundedBody === void 0) return void 0;
|
|
1942
|
+
return Object.freeze({
|
|
1943
|
+
title: boundedTitle,
|
|
1944
|
+
body: boundedBody
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
/** Extract the concatenated text parts of an assistant message event. */
|
|
1948
|
+
function assistantMessageText(event) {
|
|
1949
|
+
const content = record(record(event.data)?.message)?.content;
|
|
1950
|
+
if (typeof content === "string") return content === "" ? void 0 : content;
|
|
1951
|
+
if (!Array.isArray(content)) return void 0;
|
|
1952
|
+
let text = "";
|
|
1953
|
+
for (const part of content) {
|
|
1954
|
+
const block = record(part);
|
|
1955
|
+
if (block?.type === "text" && typeof block.text === "string") text += block.text;
|
|
1956
|
+
}
|
|
1957
|
+
return text === "" ? void 0 : text;
|
|
1958
|
+
}
|
|
1959
|
+
function firstNonEmptyLine(value) {
|
|
1960
|
+
if (value === void 0) return void 0;
|
|
1961
|
+
for (const line of value.split(/\r\n|\r|\n/gu)) {
|
|
1962
|
+
const trimmed = line.trim();
|
|
1963
|
+
if (trimmed !== "") return trimmed;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
function sessionTitle(reader, session) {
|
|
1967
|
+
if (reader === void 0) return void 0;
|
|
1968
|
+
try {
|
|
1969
|
+
const value = reader(session);
|
|
1970
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
1971
|
+
} catch {
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Subscribe to completed root turns. Subagent turns are skipped so one task
|
|
1977
|
+
* announces once, and rapid turn boundaries collapse into a single event.
|
|
1978
|
+
* Returns a disposer that also drops pending debounces.
|
|
1979
|
+
*/
|
|
1980
|
+
function watchTaskCompletions(ctx, options) {
|
|
1981
|
+
const debounceMs = options.debounceMs ?? 1e3;
|
|
1982
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1983
|
+
/** First non-empty assistant line of the latest turn per root Session. */
|
|
1984
|
+
const assistantLines = /* @__PURE__ */ new Map();
|
|
1985
|
+
const rememberLine = (sessionId, turn, line) => {
|
|
1986
|
+
if (assistantLines.size >= MAX_PENDING_TEXT && !assistantLines.has(sessionId)) {
|
|
1987
|
+
const oldest = assistantLines.keys().next();
|
|
1988
|
+
if (!oldest.done) assistantLines.delete(oldest.value);
|
|
1989
|
+
}
|
|
1990
|
+
const existing = assistantLines.get(sessionId);
|
|
1991
|
+
if (existing === void 0 || existing.turn !== turn) assistantLines.set(sessionId, {
|
|
1992
|
+
turn,
|
|
1993
|
+
line
|
|
1994
|
+
});
|
|
1995
|
+
};
|
|
1996
|
+
const disposeListener = ctx.on("session/event", (session, event) => {
|
|
1997
|
+
if (session.header?.parentSession !== void 0 && session.header?.parentSession !== null) return;
|
|
1998
|
+
const sessionId = String(session.id);
|
|
1999
|
+
if (event.type === "turn/start") {
|
|
2000
|
+
assistantLines.delete(sessionId);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (event.type === "assistant/message") {
|
|
2004
|
+
const turn = turnNumber(record(event.data)?.turn);
|
|
2005
|
+
const line = firstNonEmptyLine(assistantMessageText(event));
|
|
2006
|
+
if (line !== void 0) rememberLine(sessionId, turn, line);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (event.type !== "turn/end") return;
|
|
2010
|
+
const { turn, completed } = turnData(event.data);
|
|
2011
|
+
if (!completed) {
|
|
2012
|
+
assistantLines.delete(sessionId);
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const pendingLine = assistantLines.get(sessionId);
|
|
2016
|
+
const announcement = taskAnnouncementText(sessionTitle(options.readSessionTitle, session), pendingLine?.turn === turn ? pendingLine.line : void 0);
|
|
2017
|
+
assistantLines.delete(sessionId);
|
|
2018
|
+
const previous = pending.get(sessionId);
|
|
2019
|
+
if (previous !== void 0) clearTimeout(previous);
|
|
2020
|
+
pending.set(sessionId, setTimeout(() => {
|
|
2021
|
+
pending.delete(sessionId);
|
|
2022
|
+
options.log?.("task-completed-announced", {
|
|
2023
|
+
sessionId,
|
|
2024
|
+
turn
|
|
2025
|
+
});
|
|
2026
|
+
options.onTaskCompleted(Object.freeze({
|
|
2027
|
+
sessionId,
|
|
2028
|
+
turn,
|
|
2029
|
+
...announcement === void 0 ? {} : announcement
|
|
2030
|
+
}));
|
|
2031
|
+
}, debounceMs));
|
|
2032
|
+
});
|
|
2033
|
+
return () => {
|
|
2034
|
+
disposeListener();
|
|
2035
|
+
for (const timer of pending.values()) clearTimeout(timer);
|
|
2036
|
+
pending.clear();
|
|
2037
|
+
assistantLines.clear();
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
/** One subscription feeding every live gateway; gateways register on start. */
|
|
2041
|
+
var TaskEventHub = class {
|
|
2042
|
+
sinks = /* @__PURE__ */ new Set();
|
|
2043
|
+
/** Register a sink; returns its disposer. */
|
|
2044
|
+
add(sink) {
|
|
2045
|
+
this.sinks.add(sink);
|
|
2046
|
+
return () => {
|
|
2047
|
+
this.sinks.delete(sink);
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
/** Fan out to a snapshot so a failing sink cannot break its siblings. */
|
|
2051
|
+
broadcast(event) {
|
|
2052
|
+
for (const sink of [...this.sinks]) try {
|
|
2053
|
+
sink.broadcastTaskEvent(event);
|
|
2054
|
+
} catch {}
|
|
2055
|
+
}
|
|
2056
|
+
/** Visible for tests. */
|
|
2057
|
+
get size() {
|
|
2058
|
+
return this.sinks.size;
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
1867
2061
|
function unwrapBrackets(hostname) {
|
|
1868
2062
|
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
1869
2063
|
}
|
|
@@ -2156,6 +2350,8 @@ function requestPathCategory(path) {
|
|
|
2156
2350
|
case "/mobile-access/events": return path;
|
|
2157
2351
|
default:
|
|
2158
2352
|
if (path.startsWith("/api/")) return "/api/*";
|
|
2353
|
+
if (path.startsWith("/plugins/")) return "/plugins/*";
|
|
2354
|
+
if (path.startsWith("/assets/")) return "/assets/*";
|
|
2159
2355
|
if (path.startsWith("/mobile-access/extensions/")) return "/mobile-access/extensions/*";
|
|
2160
2356
|
if (/^\/mobile-access\/compat-[a-f\d]{64}\.js$/u.test(path)) return "/mobile-access/compat.js";
|
|
2161
2357
|
if (path.startsWith("/mobile-access/")) return "/mobile-access/*";
|
|
@@ -3278,7 +3474,7 @@ function forwardableClose(code) {
|
|
|
3278
3474
|
* @param upstreamCookie - gateway's signed upstream cookie; browser cookies are discarded.
|
|
3279
3475
|
* @param options - resolved message, queue, and compression limits.
|
|
3280
3476
|
* @param handshakeTimeoutMs - maximum handshake and close-handshake duration.
|
|
3281
|
-
* @returns
|
|
3477
|
+
* @returns the close handle after both HTTP upgrades are accepted, or rejection before acceptance.
|
|
3282
3478
|
*/
|
|
3283
3479
|
function bridgeCompressedWebSocket(request, client, upstream, head, target, upstreamCookie, options, handshakeTimeoutMs) {
|
|
3284
3480
|
return new Promise((resolve, reject) => {
|
|
@@ -3467,7 +3663,7 @@ function bridgeCompressedWebSocket(request, client, upstream, head, target, upst
|
|
|
3467
3663
|
accepted = true;
|
|
3468
3664
|
clearTimeout(handshakeTimer);
|
|
3469
3665
|
incoming.resume();
|
|
3470
|
-
resolve();
|
|
3666
|
+
resolve({ close: beginClose });
|
|
3471
3667
|
});
|
|
3472
3668
|
} catch (error) {
|
|
3473
3669
|
stop(error instanceof Error ? error : /* @__PURE__ */ new Error("Client WebSocket upgrade failed"));
|
|
@@ -9375,14 +9571,28 @@ var MobileAccessGateway = class {
|
|
|
9375
9571
|
this.extensionEventRevision += 1;
|
|
9376
9572
|
for (const listener of this.extensionEventListeners) listener(this.extensionEventRevision);
|
|
9377
9573
|
}
|
|
9378
|
-
/**
|
|
9574
|
+
/**
|
|
9575
|
+
* Fan a completed task to every phone holding this gateway's event stream.
|
|
9576
|
+
*
|
|
9577
|
+
* `sessionId` and `turn` always stay; `title`/`body` are optional and are
|
|
9578
|
+
* emitted together so an older App ignores them and falls back to its own
|
|
9579
|
+
* localized text. This layer re-bounds the strings (code-point safe) instead
|
|
9580
|
+
* of trusting the caller; redaction is deliberately owned only by the
|
|
9581
|
+
* task-events derivation, not repeated here, because this method is also
|
|
9582
|
+
* callable by fixtures that bypass the watcher.
|
|
9583
|
+
*/
|
|
9379
9584
|
broadcastTaskEvent(event) {
|
|
9380
9585
|
if (this.closing) return;
|
|
9381
|
-
const payload =
|
|
9586
|
+
const payload = {
|
|
9382
9587
|
sessionId: String(event.sessionId),
|
|
9383
9588
|
turn: Number(event.turn) || 0
|
|
9384
|
-
}
|
|
9385
|
-
|
|
9589
|
+
};
|
|
9590
|
+
if (typeof event.title === "string" && event.title !== "" && typeof event.body === "string" && event.body !== "") {
|
|
9591
|
+
payload.title = Array.from(event.title).slice(0, TASK_ANNOUNCEMENT_LIMITS.title).join("");
|
|
9592
|
+
payload.body = Array.from(event.body).slice(0, TASK_ANNOUNCEMENT_LIMITS.body).join("");
|
|
9593
|
+
}
|
|
9594
|
+
const serialized = JSON.stringify(payload);
|
|
9595
|
+
for (const listener of this.taskEventListeners) listener(serialized);
|
|
9386
9596
|
}
|
|
9387
9597
|
/** Notify only the device whose persistent credential was revoked by the Host. */
|
|
9388
9598
|
broadcastDeviceRevoked(deviceId) {
|
|
@@ -9623,9 +9833,31 @@ var MobileAccessGateway = class {
|
|
|
9623
9833
|
client.destroy();
|
|
9624
9834
|
upstream.destroy();
|
|
9625
9835
|
};
|
|
9836
|
+
let closeCompressedBridge;
|
|
9837
|
+
const finishGracefully = () => {
|
|
9838
|
+
if (closeCompressedBridge !== void 0) closeCompressedBridge(1e3, "session expired");
|
|
9839
|
+
else {
|
|
9840
|
+
upstream.unpipe(client);
|
|
9841
|
+
client.unpipe(upstream);
|
|
9842
|
+
upstream.pause();
|
|
9843
|
+
const sendCloseFrame = (socket) => {
|
|
9844
|
+
if (socket.destroyed || !socket.writable) return;
|
|
9845
|
+
try {
|
|
9846
|
+
socket.write(Buffer.from([
|
|
9847
|
+
136,
|
|
9848
|
+
2,
|
|
9849
|
+
3,
|
|
9850
|
+
232
|
|
9851
|
+
]));
|
|
9852
|
+
} catch {}
|
|
9853
|
+
};
|
|
9854
|
+
sendCloseFrame(client);
|
|
9855
|
+
}
|
|
9856
|
+
setTimeout(closeBoth, 50).unref();
|
|
9857
|
+
};
|
|
9626
9858
|
client.on("error", closeBoth);
|
|
9627
9859
|
upstream.on("error", closeBoth);
|
|
9628
|
-
const timer = setTimeout(
|
|
9860
|
+
const timer = setTimeout(finishGracefully, Math.max(1, authorization.expiresAt - Date.now()));
|
|
9629
9861
|
timer.unref();
|
|
9630
9862
|
const record = Object.freeze({
|
|
9631
9863
|
...authorization,
|
|
@@ -9654,7 +9886,10 @@ var MobileAccessGateway = class {
|
|
|
9654
9886
|
const compressionTarget = new URL(this.config.upstreamOrigin.href);
|
|
9655
9887
|
compressionTarget.pathname = target.pathname;
|
|
9656
9888
|
compressionTarget.search = target.search;
|
|
9657
|
-
await bridgeCompressedWebSocket(request, client, upstream, head, compressionTarget, upstreamCookie, this.config.websocketCompression, this.config.upstreamTimeoutMs);
|
|
9889
|
+
const bridge = await bridgeCompressedWebSocket(request, client, upstream, head, compressionTarget, upstreamCookie, this.config.websocketCompression, this.config.upstreamTimeoutMs);
|
|
9890
|
+
closeCompressedBridge = (code, reason) => {
|
|
9891
|
+
bridge.close(code, reason);
|
|
9892
|
+
};
|
|
9658
9893
|
return;
|
|
9659
9894
|
}
|
|
9660
9895
|
await new Promise((resolve, reject) => {
|
|
@@ -16873,83 +17108,6 @@ function irohGatewayConfig(template, endpointId, stateFile, tls, listenPort = 0)
|
|
|
16873
17108
|
pairingTtlMs: 3e5
|
|
16874
17109
|
});
|
|
16875
17110
|
}
|
|
16876
|
-
// src/task-events.ts
|
|
16877
|
-
const TASK_EVENT_DEBOUNCE_MS = 1e3;
|
|
16878
|
-
function turnNumber(value) {
|
|
16879
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
16880
|
-
}
|
|
16881
|
-
function turnData(value) {
|
|
16882
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return {
|
|
16883
|
-
turn: 0,
|
|
16884
|
-
completed: false
|
|
16885
|
-
};
|
|
16886
|
-
const record = value;
|
|
16887
|
-
const reason = record.reason;
|
|
16888
|
-
const completed = typeof reason === "string" ? reason === "completed" : reason !== null && typeof reason === "object" && reason.kind === "completed";
|
|
16889
|
-
return {
|
|
16890
|
-
turn: turnNumber(record.turn),
|
|
16891
|
-
completed
|
|
16892
|
-
};
|
|
16893
|
-
}
|
|
16894
|
-
/**
|
|
16895
|
-
* Subscribe to completed root turns. Subagent turns are skipped so one task
|
|
16896
|
-
* announces once, and rapid turn boundaries collapse into a single event.
|
|
16897
|
-
* Returns a disposer that also drops pending debounces.
|
|
16898
|
-
*/
|
|
16899
|
-
function watchTaskCompletions(ctx, options) {
|
|
16900
|
-
const debounceMs = options.debounceMs ?? 1e3;
|
|
16901
|
-
const pending = /* @__PURE__ */ new Map();
|
|
16902
|
-
const disposeListener = ctx.on("session/event", (session, event) => {
|
|
16903
|
-
if (event.type !== "turn/end") return;
|
|
16904
|
-
const { turn, completed } = turnData(event.data);
|
|
16905
|
-
if (!completed) return;
|
|
16906
|
-
if (session.header?.parentSession !== void 0 && session.header?.parentSession !== null) return;
|
|
16907
|
-
const sessionId = String(session.id);
|
|
16908
|
-
options.log?.("task-completed-observed", {
|
|
16909
|
-
sessionId,
|
|
16910
|
-
turn
|
|
16911
|
-
});
|
|
16912
|
-
const previous = pending.get(sessionId);
|
|
16913
|
-
if (previous !== void 0) clearTimeout(previous);
|
|
16914
|
-
pending.set(sessionId, setTimeout(() => {
|
|
16915
|
-
pending.delete(sessionId);
|
|
16916
|
-
options.log?.("task-completed-announced", {
|
|
16917
|
-
sessionId,
|
|
16918
|
-
turn
|
|
16919
|
-
});
|
|
16920
|
-
options.onTaskCompleted(Object.freeze({
|
|
16921
|
-
sessionId,
|
|
16922
|
-
turn
|
|
16923
|
-
}));
|
|
16924
|
-
}, debounceMs));
|
|
16925
|
-
});
|
|
16926
|
-
return () => {
|
|
16927
|
-
disposeListener();
|
|
16928
|
-
for (const timer of pending.values()) clearTimeout(timer);
|
|
16929
|
-
pending.clear();
|
|
16930
|
-
};
|
|
16931
|
-
}
|
|
16932
|
-
/** One subscription feeding every live gateway; gateways register on start. */
|
|
16933
|
-
var TaskEventHub = class {
|
|
16934
|
-
sinks = /* @__PURE__ */ new Set();
|
|
16935
|
-
/** Register a sink; returns its disposer. */
|
|
16936
|
-
add(sink) {
|
|
16937
|
-
this.sinks.add(sink);
|
|
16938
|
-
return () => {
|
|
16939
|
-
this.sinks.delete(sink);
|
|
16940
|
-
};
|
|
16941
|
-
}
|
|
16942
|
-
/** Fan out to a snapshot so a failing sink cannot break its siblings. */
|
|
16943
|
-
broadcast(event) {
|
|
16944
|
-
for (const sink of [...this.sinks]) try {
|
|
16945
|
-
sink.broadcastTaskEvent(event);
|
|
16946
|
-
} catch {}
|
|
16947
|
-
}
|
|
16948
|
-
/** Visible for tests. */
|
|
16949
|
-
get size() {
|
|
16950
|
-
return this.sinks.size;
|
|
16951
|
-
}
|
|
16952
|
-
};
|
|
16953
17111
|
// src/diagnostic-network-context.ts
|
|
16954
17112
|
function addressKind(address) {
|
|
16955
17113
|
if (isIP(address) === 4) {
|
|
@@ -21306,6 +21464,74 @@ const OUTCOME_COALESCE_MS = 1e3;
|
|
|
21306
21464
|
const ATTENTION_TTL_MS = 3e5;
|
|
21307
21465
|
const OUTCOME_TTL_MS = 18e5;
|
|
21308
21466
|
const ATTEMPT_TIMEOUT_MS = 1e4;
|
|
21467
|
+
// src/mobile-push/ownership.ts
|
|
21468
|
+
const MAX_OWNERSHIP_DEPTH = 64;
|
|
21469
|
+
/**
|
|
21470
|
+
* Read the durable header from the object a public event delivered.
|
|
21471
|
+
*
|
|
21472
|
+
* A `session/event` observer receives the Session itself, whose header lives on
|
|
21473
|
+
* the live Agent; a request can carry an Agent-shaped identity without the
|
|
21474
|
+
* Session attached. Resolve both through the public registry instead of
|
|
21475
|
+
* assuming one shape.
|
|
21476
|
+
*/
|
|
21477
|
+
function durableHeader(registry, agent) {
|
|
21478
|
+
const direct = agent.session?.header;
|
|
21479
|
+
if (direct !== void 0) return direct;
|
|
21480
|
+
return registry?.get?.(agent.id)?.session?.header;
|
|
21481
|
+
}
|
|
21482
|
+
/**
|
|
21483
|
+
* Find the owning parent recorded by the durable Session header.
|
|
21484
|
+
*
|
|
21485
|
+
* Only a header that explicitly classifies the Session as a subagent child
|
|
21486
|
+
* creates ownership. Historical ordinary forks may retain a `parentSession`
|
|
21487
|
+
* source id, so the field alone is not evidence of ownership.
|
|
21488
|
+
*/
|
|
21489
|
+
function owningParentId(registry, agent) {
|
|
21490
|
+
const header = durableHeader(registry, agent);
|
|
21491
|
+
if (header?.origin !== "subagent") return void 0;
|
|
21492
|
+
const parent = header.parentSession;
|
|
21493
|
+
return typeof parent === "string" && parent.length > 0 ? parent : void 0;
|
|
21494
|
+
}
|
|
21495
|
+
/** The public ownership walk available before DSH 0.2.2-rc.2, when it exists. */
|
|
21496
|
+
function legacyOwnerOf(registry) {
|
|
21497
|
+
if (typeof registry?.list !== "function" || typeof registry.isOwnedBy !== "function") return void 0;
|
|
21498
|
+
return (id) => (registry?.list?.() ?? []).find((candidate) => registry?.isOwnedBy?.(id, candidate) === true);
|
|
21499
|
+
}
|
|
21500
|
+
/**
|
|
21501
|
+
* Whether this Agent is a runtime child that must not notify on its own.
|
|
21502
|
+
*
|
|
21503
|
+
* Without the removed registry ownership API, the live parent Agent recorded
|
|
21504
|
+
* by the header is the public evidence; an orphan does not count as owned,
|
|
21505
|
+
* matching the earlier walk that stopped at a missing owner.
|
|
21506
|
+
*/
|
|
21507
|
+
function isPushRuntimeChild(registry, agent) {
|
|
21508
|
+
const ownerOf = legacyOwnerOf(registry);
|
|
21509
|
+
if (ownerOf !== void 0) return ownerOf(agent.id) !== void 0;
|
|
21510
|
+
const parentId = owningParentId(registry, agent);
|
|
21511
|
+
return parentId !== void 0 && registry?.get?.(parentId) !== void 0;
|
|
21512
|
+
}
|
|
21513
|
+
/**
|
|
21514
|
+
* Resolve the root Session a notification should target.
|
|
21515
|
+
*
|
|
21516
|
+
* Uses the removed `isOwnedBy` walk when the host still exposes it, and the
|
|
21517
|
+
* durable subagent header ancestry otherwise. A child whose parent Agent is no
|
|
21518
|
+
* longer live remains its own root, matching the previous behavior.
|
|
21519
|
+
*/
|
|
21520
|
+
function pushRootAgent(registry, agent) {
|
|
21521
|
+
const ownerOf = legacyOwnerOf(registry);
|
|
21522
|
+
let current = agent;
|
|
21523
|
+
for (let count = 0; count < MAX_OWNERSHIP_DEPTH; count++) {
|
|
21524
|
+
let parent;
|
|
21525
|
+
if (ownerOf !== void 0) parent = ownerOf(current.id);
|
|
21526
|
+
else {
|
|
21527
|
+
const parentId = owningParentId(registry, current);
|
|
21528
|
+
parent = parentId === void 0 ? void 0 : registry?.get?.(parentId);
|
|
21529
|
+
}
|
|
21530
|
+
if (parent === void 0 || parent.id === current.id) return current;
|
|
21531
|
+
current = parent;
|
|
21532
|
+
}
|
|
21533
|
+
return current;
|
|
21534
|
+
}
|
|
21309
21535
|
// src/mobile-push/requests.ts
|
|
21310
21536
|
var RequestLifetimes = class {
|
|
21311
21537
|
#records = /* @__PURE__ */ new Map();
|
|
@@ -21467,15 +21693,7 @@ var RequestLifetimes = class {
|
|
|
21467
21693
|
return rec;
|
|
21468
21694
|
}
|
|
21469
21695
|
#root(agent) {
|
|
21470
|
-
|
|
21471
|
-
if (!registry?.isOwnedBy || !registry.get || !registry.list) return agent;
|
|
21472
|
-
let current = agent;
|
|
21473
|
-
for (let count = 0; count < 64; count++) {
|
|
21474
|
-
const parent = registry.list().find((candidate) => registry.isOwnedBy?.(current.id, candidate));
|
|
21475
|
-
if (!parent) break;
|
|
21476
|
-
current = parent;
|
|
21477
|
-
}
|
|
21478
|
-
return current;
|
|
21696
|
+
return pushRootAgent(this.#ctx.agents, agent);
|
|
21479
21697
|
}
|
|
21480
21698
|
#arm(rec) {
|
|
21481
21699
|
if (rec.timer) return;
|
|
@@ -21650,20 +21868,10 @@ var OutcomeLifetimes = class {
|
|
|
21650
21868
|
}
|
|
21651
21869
|
}
|
|
21652
21870
|
#isRuntimeChild(agent) {
|
|
21653
|
-
|
|
21654
|
-
if (!registry?.isOwnedBy || !registry.list) return false;
|
|
21655
|
-
return registry.list().some((owner) => registry.isOwnedBy?.(agent.id, owner));
|
|
21871
|
+
return isPushRuntimeChild(this.#ctx.agents, agent);
|
|
21656
21872
|
}
|
|
21657
21873
|
#root(agent) {
|
|
21658
|
-
|
|
21659
|
-
if (!registry?.isOwnedBy || !registry.list) return agent;
|
|
21660
|
-
let current = agent;
|
|
21661
|
-
for (let count = 0; count < 64; count++) {
|
|
21662
|
-
const owner = registry.list().find((candidate) => registry.isOwnedBy?.(current.id, candidate));
|
|
21663
|
-
if (!owner) return current;
|
|
21664
|
-
current = owner;
|
|
21665
|
-
}
|
|
21666
|
-
return current;
|
|
21874
|
+
return pushRootAgent(this.#ctx.agents, agent);
|
|
21667
21875
|
}
|
|
21668
21876
|
};
|
|
21669
21877
|
// src/mobile-push/reconcile.ts
|
|
@@ -21940,7 +22148,7 @@ var PushReconciler = class {
|
|
|
21940
22148
|
};
|
|
21941
22149
|
// src/mobile-push/host.ts
|
|
21942
22150
|
function hasContract(ctx) {
|
|
21943
|
-
return typeof ctx.on === "function" && typeof ctx.sessionProjections?.stateOf === "function" && typeof ctx.sessionProjections.snapshot === "function" && typeof ctx.sessionProjections.register === "function" && typeof ctx.sessionQuery?.observeSession === "function" && typeof ctx.agents?.get === "function" && typeof ctx.agents.list === "function" &&
|
|
22151
|
+
return typeof ctx.on === "function" && typeof ctx.sessionProjections?.stateOf === "function" && typeof ctx.sessionProjections.snapshot === "function" && typeof ctx.sessionProjections.register === "function" && typeof ctx.sessionQuery?.observeSession === "function" && typeof ctx.agents?.get === "function" && typeof ctx.agents.list === "function" && ctx.userQuestions !== void 0 && ctx.approval !== void 0;
|
|
21944
22152
|
}
|
|
21945
22153
|
const MAINTENANCE_INTERVAL_MS = 3e4;
|
|
21946
22154
|
function createPushHost(ctx) {
|
|
@@ -24096,6 +24304,7 @@ var PushDelivery = class {
|
|
|
24096
24304
|
if (this.#closed) return;
|
|
24097
24305
|
if (this.#wakeTimer) clearTimeout(this.#wakeTimer);
|
|
24098
24306
|
this.#wakeTimer = void 0;
|
|
24307
|
+
if (this.#active >= 4) return;
|
|
24099
24308
|
this.#pruneDeferred();
|
|
24100
24309
|
let nextAt = Number.POSITIVE_INFINITY;
|
|
24101
24310
|
try {
|
|
@@ -24874,6 +25083,9 @@ async function apply(ctx, config) {
|
|
|
24874
25083
|
const taskEventHub = new TaskEventHub();
|
|
24875
25084
|
const disposeTaskEvents = watchTaskCompletions(ctx, {
|
|
24876
25085
|
onTaskCompleted: (event) => taskEventHub.broadcast(event),
|
|
25086
|
+
readSessionTitle: (session) => {
|
|
25087
|
+
return (ctx.get?.("sessionProjections"))?.stateOf?.(session, "title");
|
|
25088
|
+
},
|
|
24877
25089
|
log(event, fields) {
|
|
24878
25090
|
logger.info("task event=%s fields=%o", event, fields);
|
|
24879
25091
|
}
|