@athenaintel/react 0.41.0 → 0.41.2
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 +10 -0
- package/dist/agent-runtime/src/protocol.cjs +15 -0
- package/dist/agent-runtime/src/protocol.cjs.map +1 -1
- package/dist/agent-runtime/src/protocol.js +16 -1
- package/dist/agent-runtime/src/protocol.js.map +1 -1
- package/dist/agent-runtime/src/thread-snapshot-cache.cjs +2 -1
- package/dist/agent-runtime/src/thread-snapshot-cache.cjs.map +1 -1
- package/dist/agent-runtime/src/thread-snapshot-cache.js +2 -1
- package/dist/agent-runtime/src/thread-snapshot-cache.js.map +1 -1
- package/dist/chat-ui/lib/asset-urls.cjs +6 -1
- package/dist/chat-ui/lib/asset-urls.cjs.map +1 -1
- package/dist/chat-ui/lib/asset-urls.d.ts +6 -1
- package/dist/chat-ui/lib/asset-urls.js +7 -2
- package/dist/chat-ui/lib/asset-urls.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/athena-urls.cjs +17 -0
- package/dist/lib/athena-urls.cjs.map +1 -1
- package/dist/lib/athena-urls.d.ts +14 -0
- package/dist/lib/athena-urls.js +17 -0
- package/dist/lib/athena-urls.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.cjs +4 -14
- package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.d.ts +5 -4
- package/dist/runtime/useAthenaStatewireRuntime.js +4 -14
- package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
- package/dist/workbench/session-workbench-model.cjs +0 -18
- package/dist/workbench/session-workbench-model.cjs.map +1 -1
- package/dist/workbench/session-workbench-model.d.ts +0 -13
- package/dist/workbench/session-workbench-model.js +0 -18
- package/dist/workbench/session-workbench-model.js.map +1 -1
- package/dist/workbench/session-workbench-panes.cjs +1 -1
- package/dist/workbench/session-workbench-panes.cjs.map +1 -1
- package/dist/workbench/session-workbench-panes.js +2 -2
- package/dist/workbench/session-workbench-panes.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -807,6 +807,16 @@ Both use the same bearer / API key as session creation. The full recipe
|
|
|
807
807
|
template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
|
|
808
808
|
exported if you hold the bridge state yourself.
|
|
809
809
|
|
|
810
|
+
## What's new in 0.41.2
|
|
811
|
+
|
|
812
|
+
**A session opened while its run is streaming shows, and keeps, its own model.** `useAthenaThreadModel()` could report `ready` with no model before the session's snapshot had arrived. On a session whose run was streaming, the connection reported `connected` before the attach snapshot applied, and the SDK took the placeholder the thread shows meanwhile, which holds no server config, as what the server sent. A picker seeded from it showed the agent's default model, and the provider then wrote that pick to the session (`config/patch {model: null}`), so the session lost its model for its next turns. `ready`, and every config write the provider decides, now wait for a state the server sent: the placeholder and a remembered snapshot never count, whenever the connection reports `connected`. The `statewire.attach` diagnostics span (time to the session's snapshot) now ends when that snapshot arrives, not when the thread first renders.
|
|
813
|
+
|
|
814
|
+
Nothing to change in your app.
|
|
815
|
+
|
|
816
|
+
## What's new in 0.41.1
|
|
817
|
+
|
|
818
|
+
**A chip in a computer app links to Athena, not to the app.** In an app a computer serves, Marathon hands the provider the app's own origin as its `appUrl`, so every asset and citation chip linked to `https://<app>/dashboard/spaces/…`, the app itself. Chips now link to the deployment's Athena frontend there (`https://staging-app.athenaintel.com/…` on staging), as the workbench's "Open in Athena" links already did. This matters most for a repository file citation, which 0.41.0 opens as its link. `athenaLinkAppUrl` is now exported from the URL helpers (still from the package root). An `appUrl` you configure that is not the page's own origin is used as configured.
|
|
819
|
+
|
|
810
820
|
## What's new in 0.41.0
|
|
811
821
|
|
|
812
822
|
**Citation chips say where they point, with an icon, in every app.** An asset chip in a chat
|
|
@@ -18,6 +18,19 @@ function projectDeepAgentProtocol(state) {
|
|
|
18
18
|
clientVersion: runconfigContract.DEEP_AGENT_PROTOCOL_VERSION
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
const seedProtocolLanes = /* @__PURE__ */ new WeakSet();
|
|
22
|
+
function withSeedProtocolLane(state) {
|
|
23
|
+
const lane = state.protocol;
|
|
24
|
+
if (typeof lane !== "object" || lane === null) return state;
|
|
25
|
+
const seeded = { ...lane };
|
|
26
|
+
seedProtocolLanes.add(seeded);
|
|
27
|
+
return { ...state, protocol: seeded };
|
|
28
|
+
}
|
|
29
|
+
function isDeepAgentHostState(state) {
|
|
30
|
+
if (typeof state !== "object" || state === null) return false;
|
|
31
|
+
const lane = state[runconfigContract.DEEP_AGENT_PROTOCOL_STATE_KEY];
|
|
32
|
+
return typeof lane === "object" && lane !== null && readDeepAgentProtocol(state) !== null && !seedProtocolLanes.has(lane);
|
|
33
|
+
}
|
|
21
34
|
function withDeclaredProtocolVersion(headers) {
|
|
22
35
|
if (runconfigContract.DEEP_AGENT_PROTOCOL_VERSION_HEADER in headers) return headers;
|
|
23
36
|
return {
|
|
@@ -25,7 +38,9 @@ function withDeclaredProtocolVersion(headers) {
|
|
|
25
38
|
[runconfigContract.DEEP_AGENT_PROTOCOL_VERSION_HEADER]: String(runconfigContract.DEEP_AGENT_PROTOCOL_VERSION)
|
|
26
39
|
};
|
|
27
40
|
}
|
|
41
|
+
exports.isDeepAgentHostState = isDeepAgentHostState;
|
|
28
42
|
exports.projectDeepAgentProtocol = projectDeepAgentProtocol;
|
|
29
43
|
exports.readDeepAgentProtocol = readDeepAgentProtocol;
|
|
30
44
|
exports.withDeclaredProtocolVersion = withDeclaredProtocolVersion;
|
|
45
|
+
exports.withSeedProtocolLane = withSeedProtocolLane;
|
|
31
46
|
//# sourceMappingURL=protocol.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.cjs","sources":["../../../../agent-runtime/src/protocol.ts"],"sourcesContent":["import {\n DEEP_AGENT_PROTOCOL_STATE_KEY,\n DEEP_AGENT_PROTOCOL_VERSION,\n DEEP_AGENT_PROTOCOL_VERSION_HEADER,\n} from './generated/runconfig-contract';\n\n/**\n * Wire-contract version declaration between the deep-agent statewire host and\n * this client build (the client half of agora's\n * `agora/graphs/athena_deep_agent/protocol_version.py`; upstream ask\n * assistant-ui/statewire #31506 would make this a library concern).\n *\n * - Declaration: {@link withDeclaredProtocolVersion} stamps the version this\n * build speaks (`DEEP_AGENT_PROTOCOL_VERSION`, generated from the Python\n * source of truth) onto the attach headers — they ride the first WS frame's\n * mandatory `headers` field / the SSE request headers, so no statewire\n * change is needed. The host records it for attach telemetry and\n * per-version behaviour selection only.\n * - Advertisement: the host publishes its own `version` under the `protocol`\n * state key in every attach snapshot; {@link readDeepAgentProtocol} /\n * {@link projectDeepAgentProtocol} read it.\n *\n * There is no minimum client version: the host refuses no command for the\n * version its client declared (or for declaring none), so nothing here\n * compares versions or gates a send. Hosts still publish a `minClientVersion`\n * key (the constant 1) for installed clients whose lane reader required it;\n * this reader ignores it.\n *\n * Skew-tolerant on both sides: an old server advertises nothing (project\n * `null`, gate nothing), and a malformed lane reads as absent — the\n * declaration must never brick a client that could otherwise talk.\n */\nexport interface DeepAgentWireProtocol {\n /** The wire-contract version the server speaks. */\n version: number;\n}\n\n/** The advertised-lane projection a host surface reads. */\nexport interface DeepAgentProtocolStatus {\n /** Server-advertised current contract version. */\n protocolVersion: number;\n /** The contract version THIS client build declares. */\n clientVersion: number;\n}\n\nconst isContractVersion = (value: unknown): value is number =>\n typeof value === 'number' && Number.isInteger(value) && value >= 1;\n\n/**\n * The server-advertised version lane off a raw statewire state, or `null`\n * while it is unreadable — pre-attach, a pre-negotiation server's snapshot\n * (no lane), or a malformed lane (tolerated as absent, never thrown on).\n */\nexport function readDeepAgentProtocol(state: unknown): DeepAgentWireProtocol | null {\n if (typeof state !== 'object' || state === null) return null;\n const lane = (state as Record<string, unknown>)[DEEP_AGENT_PROTOCOL_STATE_KEY];\n if (typeof lane !== 'object' || lane === null) return null;\n const { version } = lane as Record<string, unknown>;\n if (!isContractVersion(version)) return null;\n return { version };\n}\n\n/**\n * Project the advertised version for a host surface, or `null` while the\n * server has not advertised (see {@link readDeepAgentProtocol}). Pure and\n * selector-safe, mirroring `readSessionPersisted`.\n */\nexport function projectDeepAgentProtocol(state: unknown): DeepAgentProtocolStatus | null {\n const lane = readDeepAgentProtocol(state);\n if (lane === null) return null;\n return {\n protocolVersion: lane.version,\n clientVersion: DEEP_AGENT_PROTOCOL_VERSION,\n };\n}\n\n/**\n * Attach headers with this build's contract version declared. Applied by\n * `useDeepAgentThread` to every resolved header set, so all surfaces declare\n * without per-host wiring; an explicit entry (tests, forced overrides) wins.\n */\nexport function withDeclaredProtocolVersion(\n headers: Record<string, string>\n): Record<string, string> {\n if (DEEP_AGENT_PROTOCOL_VERSION_HEADER in headers) return headers;\n return {\n ...headers,\n [DEEP_AGENT_PROTOCOL_VERSION_HEADER]: String(DEEP_AGENT_PROTOCOL_VERSION),\n };\n}\n"],"names":["DEEP_AGENT_PROTOCOL_STATE_KEY","DEEP_AGENT_PROTOCOL_VERSION","DEEP_AGENT_PROTOCOL_VERSION_HEADER"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"protocol.cjs","sources":["../../../../agent-runtime/src/protocol.ts"],"sourcesContent":["import {\n DEEP_AGENT_PROTOCOL_STATE_KEY,\n DEEP_AGENT_PROTOCOL_VERSION,\n DEEP_AGENT_PROTOCOL_VERSION_HEADER,\n} from './generated/runconfig-contract';\nimport type { DeepAgentStreamValues } from './types';\n\n/**\n * Wire-contract version declaration between the deep-agent statewire host and\n * this client build (the client half of agora's\n * `agora/graphs/athena_deep_agent/protocol_version.py`; upstream ask\n * assistant-ui/statewire #31506 would make this a library concern).\n *\n * - Declaration: {@link withDeclaredProtocolVersion} stamps the version this\n * build speaks (`DEEP_AGENT_PROTOCOL_VERSION`, generated from the Python\n * source of truth) onto the attach headers — they ride the first WS frame's\n * mandatory `headers` field / the SSE request headers, so no statewire\n * change is needed. The host records it for attach telemetry and\n * per-version behaviour selection only.\n * - Advertisement: the host publishes its own `version` under the `protocol`\n * state key in every attach snapshot; {@link readDeepAgentProtocol} /\n * {@link projectDeepAgentProtocol} read it.\n *\n * There is no minimum client version: the host refuses no command for the\n * version its client declared (or for declaring none), so nothing here\n * compares versions or gates a send. Hosts still publish a `minClientVersion`\n * key (the constant 1) for installed clients whose lane reader required it;\n * this reader ignores it.\n *\n * Skew-tolerant on both sides: an old server advertises nothing (project\n * `null`, gate nothing), and a malformed lane reads as absent — the\n * declaration must never brick a client that could otherwise talk.\n */\nexport interface DeepAgentWireProtocol {\n /** The wire-contract version the server speaks. */\n version: number;\n}\n\n/** The advertised-lane projection a host surface reads. */\nexport interface DeepAgentProtocolStatus {\n /** Server-advertised current contract version. */\n protocolVersion: number;\n /** The contract version THIS client build declares. */\n clientVersion: number;\n}\n\nconst isContractVersion = (value: unknown): value is number =>\n typeof value === 'number' && Number.isInteger(value) && value >= 1;\n\n/**\n * The server-advertised version lane off a raw statewire state, or `null`\n * while it is unreadable — pre-attach, a pre-negotiation server's snapshot\n * (no lane), or a malformed lane (tolerated as absent, never thrown on).\n */\nexport function readDeepAgentProtocol(state: unknown): DeepAgentWireProtocol | null {\n if (typeof state !== 'object' || state === null) return null;\n const lane = (state as Record<string, unknown>)[DEEP_AGENT_PROTOCOL_STATE_KEY];\n if (typeof lane !== 'object' || lane === null) return null;\n const { version } = lane as Record<string, unknown>;\n if (!isContractVersion(version)) return null;\n return { version };\n}\n\n/**\n * Project the advertised version for a host surface, or `null` while the\n * server has not advertised (see {@link readDeepAgentProtocol}). Pure and\n * selector-safe, mirroring `readSessionPersisted`.\n */\nexport function projectDeepAgentProtocol(state: unknown): DeepAgentProtocolStatus | null {\n const lane = readDeepAgentProtocol(state);\n if (lane === null) return null;\n return {\n protocolVersion: lane.version,\n clientVersion: DEEP_AGENT_PROTOCOL_VERSION,\n };\n}\n\n/**\n * The protocol lanes seeds carry ({@link withSeedProtocolLane}), by identity.\n * Statewire's optimistic folds are copy-on-write, so every state folded over a\n * seed keeps its lane object, while a state the host sent never holds one\n * (each is parsed off the wire).\n */\nconst seedProtocolLanes = new WeakSet<object>();\n\n/**\n * `state` with a protocol lane of its own, remembered as a seed's — for the\n * remembered states a mount is seeded with (`toThreadSnapshotSeed`), whose\n * lanes are what the host sent an earlier attach. The lane is copied rather\n * than remembered in place, so a live attach still holding the original object\n * is never read as a seed.\n */\nexport function withSeedProtocolLane(state: DeepAgentStreamValues): DeepAgentStreamValues {\n const lane = state.protocol;\n if (typeof lane !== 'object' || lane === null) return state;\n const seeded = { ...lane };\n seedProtocolLanes.add(seeded);\n return { ...state, protocol: seeded };\n}\n\n/**\n * Whether `state` is one the host sent on this attach. The host advertises the\n * protocol lane in every attach snapshot and keeps it on every later frame;\n * the states a thread renders before its snapshot carry none (the placeholder\n * an existing thread loads with, and any optimistic fold over it) or a seed's\n * ({@link withSeedProtocolLane}).\n *\n * Decide anything about what the host holds — its config lane above all —\n * from these states only, never from the connection status: on a thread\n * mid-run the channel reports `connected` before the attach snapshot applies,\n * and a placeholder read as the host's says the thread has no config.\n *\n * A host predating the lane (2026-08-28) never answers true; the config lane\n * writes clients make (`config/patch`, 2026-09-05) came after it.\n */\nexport function isDeepAgentHostState(state: unknown): boolean {\n if (typeof state !== 'object' || state === null) return false;\n const lane = (state as Record<string, unknown>)[DEEP_AGENT_PROTOCOL_STATE_KEY];\n return (\n typeof lane === 'object' &&\n lane !== null &&\n readDeepAgentProtocol(state) !== null &&\n !seedProtocolLanes.has(lane)\n );\n}\n\n/**\n * Attach headers with this build's contract version declared. Applied by\n * `useDeepAgentThread` to every resolved header set, so all surfaces declare\n * without per-host wiring; an explicit entry (tests, forced overrides) wins.\n */\nexport function withDeclaredProtocolVersion(\n headers: Record<string, string>\n): Record<string, string> {\n if (DEEP_AGENT_PROTOCOL_VERSION_HEADER in headers) return headers;\n return {\n ...headers,\n [DEEP_AGENT_PROTOCOL_VERSION_HEADER]: String(DEEP_AGENT_PROTOCOL_VERSION),\n };\n}\n"],"names":["DEEP_AGENT_PROTOCOL_STATE_KEY","DEEP_AGENT_PROTOCOL_VERSION","DEEP_AGENT_PROTOCOL_VERSION_HEADER"],"mappings":";;;AA8CA,MAAM,oBAAoB,CAAC,UACzB,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS;AAO5D,SAAS,sBAAsB,OAA8C;AAClF,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,OAAQ,MAAkCA,+CAA6B;AAC7E,MAAI,OAAO,SAAS,YAAY,SAAS,KAAM,QAAO;AACtD,QAAM,EAAE,YAAY;AACpB,MAAI,CAAC,kBAAkB,OAAO,EAAG,QAAO;AACxC,SAAO,EAAE,QAAA;AACX;AAOO,SAAS,yBAAyB,OAAgD;AACvF,QAAM,OAAO,sBAAsB,KAAK;AACxC,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO;AAAA,IACL,iBAAiB,KAAK;AAAA,IACtB,eAAeC,kBAAAA;AAAAA,EAAA;AAEnB;AAQA,MAAM,wCAAwB,QAAA;AASvB,SAAS,qBAAqB,OAAqD;AACxF,QAAM,OAAO,MAAM;AACnB,MAAI,OAAO,SAAS,YAAY,SAAS,KAAM,QAAO;AACtD,QAAM,SAAS,EAAE,GAAG,KAAA;AACpB,oBAAkB,IAAI,MAAM;AAC5B,SAAO,EAAE,GAAG,OAAO,UAAU,OAAA;AAC/B;AAiBO,SAAS,qBAAqB,OAAyB;AAC5D,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,OAAQ,MAAkCD,+CAA6B;AAC7E,SACE,OAAO,SAAS,YAChB,SAAS,QACT,sBAAsB,KAAK,MAAM,QACjC,CAAC,kBAAkB,IAAI,IAAI;AAE/B;AAOO,SAAS,4BACd,SACwB;AACxB,MAAIE,kBAAAA,sCAAsC,QAAS,QAAO;AAC1D,SAAO;AAAA,IACL,GAAG;AAAA,IACH,CAACA,kBAAAA,kCAAkC,GAAG,OAAOD,kBAAAA,2BAA2B;AAAA,EAAA;AAE5E;;;;;;"}
|
|
@@ -16,6 +16,19 @@ function projectDeepAgentProtocol(state) {
|
|
|
16
16
|
clientVersion: DEEP_AGENT_PROTOCOL_VERSION
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
const seedProtocolLanes = /* @__PURE__ */ new WeakSet();
|
|
20
|
+
function withSeedProtocolLane(state) {
|
|
21
|
+
const lane = state.protocol;
|
|
22
|
+
if (typeof lane !== "object" || lane === null) return state;
|
|
23
|
+
const seeded = { ...lane };
|
|
24
|
+
seedProtocolLanes.add(seeded);
|
|
25
|
+
return { ...state, protocol: seeded };
|
|
26
|
+
}
|
|
27
|
+
function isDeepAgentHostState(state) {
|
|
28
|
+
if (typeof state !== "object" || state === null) return false;
|
|
29
|
+
const lane = state[DEEP_AGENT_PROTOCOL_STATE_KEY];
|
|
30
|
+
return typeof lane === "object" && lane !== null && readDeepAgentProtocol(state) !== null && !seedProtocolLanes.has(lane);
|
|
31
|
+
}
|
|
19
32
|
function withDeclaredProtocolVersion(headers) {
|
|
20
33
|
if (DEEP_AGENT_PROTOCOL_VERSION_HEADER in headers) return headers;
|
|
21
34
|
return {
|
|
@@ -24,8 +37,10 @@ function withDeclaredProtocolVersion(headers) {
|
|
|
24
37
|
};
|
|
25
38
|
}
|
|
26
39
|
export {
|
|
40
|
+
isDeepAgentHostState,
|
|
27
41
|
projectDeepAgentProtocol,
|
|
28
42
|
readDeepAgentProtocol,
|
|
29
|
-
withDeclaredProtocolVersion
|
|
43
|
+
withDeclaredProtocolVersion,
|
|
44
|
+
withSeedProtocolLane
|
|
30
45
|
};
|
|
31
46
|
//# sourceMappingURL=protocol.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sources":["../../../../agent-runtime/src/protocol.ts"],"sourcesContent":["import {\n DEEP_AGENT_PROTOCOL_STATE_KEY,\n DEEP_AGENT_PROTOCOL_VERSION,\n DEEP_AGENT_PROTOCOL_VERSION_HEADER,\n} from './generated/runconfig-contract';\n\n/**\n * Wire-contract version declaration between the deep-agent statewire host and\n * this client build (the client half of agora's\n * `agora/graphs/athena_deep_agent/protocol_version.py`; upstream ask\n * assistant-ui/statewire #31506 would make this a library concern).\n *\n * - Declaration: {@link withDeclaredProtocolVersion} stamps the version this\n * build speaks (`DEEP_AGENT_PROTOCOL_VERSION`, generated from the Python\n * source of truth) onto the attach headers — they ride the first WS frame's\n * mandatory `headers` field / the SSE request headers, so no statewire\n * change is needed. The host records it for attach telemetry and\n * per-version behaviour selection only.\n * - Advertisement: the host publishes its own `version` under the `protocol`\n * state key in every attach snapshot; {@link readDeepAgentProtocol} /\n * {@link projectDeepAgentProtocol} read it.\n *\n * There is no minimum client version: the host refuses no command for the\n * version its client declared (or for declaring none), so nothing here\n * compares versions or gates a send. Hosts still publish a `minClientVersion`\n * key (the constant 1) for installed clients whose lane reader required it;\n * this reader ignores it.\n *\n * Skew-tolerant on both sides: an old server advertises nothing (project\n * `null`, gate nothing), and a malformed lane reads as absent — the\n * declaration must never brick a client that could otherwise talk.\n */\nexport interface DeepAgentWireProtocol {\n /** The wire-contract version the server speaks. */\n version: number;\n}\n\n/** The advertised-lane projection a host surface reads. */\nexport interface DeepAgentProtocolStatus {\n /** Server-advertised current contract version. */\n protocolVersion: number;\n /** The contract version THIS client build declares. */\n clientVersion: number;\n}\n\nconst isContractVersion = (value: unknown): value is number =>\n typeof value === 'number' && Number.isInteger(value) && value >= 1;\n\n/**\n * The server-advertised version lane off a raw statewire state, or `null`\n * while it is unreadable — pre-attach, a pre-negotiation server's snapshot\n * (no lane), or a malformed lane (tolerated as absent, never thrown on).\n */\nexport function readDeepAgentProtocol(state: unknown): DeepAgentWireProtocol | null {\n if (typeof state !== 'object' || state === null) return null;\n const lane = (state as Record<string, unknown>)[DEEP_AGENT_PROTOCOL_STATE_KEY];\n if (typeof lane !== 'object' || lane === null) return null;\n const { version } = lane as Record<string, unknown>;\n if (!isContractVersion(version)) return null;\n return { version };\n}\n\n/**\n * Project the advertised version for a host surface, or `null` while the\n * server has not advertised (see {@link readDeepAgentProtocol}). Pure and\n * selector-safe, mirroring `readSessionPersisted`.\n */\nexport function projectDeepAgentProtocol(state: unknown): DeepAgentProtocolStatus | null {\n const lane = readDeepAgentProtocol(state);\n if (lane === null) return null;\n return {\n protocolVersion: lane.version,\n clientVersion: DEEP_AGENT_PROTOCOL_VERSION,\n };\n}\n\n/**\n * Attach headers with this build's contract version declared. Applied by\n * `useDeepAgentThread` to every resolved header set, so all surfaces declare\n * without per-host wiring; an explicit entry (tests, forced overrides) wins.\n */\nexport function withDeclaredProtocolVersion(\n headers: Record<string, string>\n): Record<string, string> {\n if (DEEP_AGENT_PROTOCOL_VERSION_HEADER in headers) return headers;\n return {\n ...headers,\n [DEEP_AGENT_PROTOCOL_VERSION_HEADER]: String(DEEP_AGENT_PROTOCOL_VERSION),\n };\n}\n"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"protocol.js","sources":["../../../../agent-runtime/src/protocol.ts"],"sourcesContent":["import {\n DEEP_AGENT_PROTOCOL_STATE_KEY,\n DEEP_AGENT_PROTOCOL_VERSION,\n DEEP_AGENT_PROTOCOL_VERSION_HEADER,\n} from './generated/runconfig-contract';\nimport type { DeepAgentStreamValues } from './types';\n\n/**\n * Wire-contract version declaration between the deep-agent statewire host and\n * this client build (the client half of agora's\n * `agora/graphs/athena_deep_agent/protocol_version.py`; upstream ask\n * assistant-ui/statewire #31506 would make this a library concern).\n *\n * - Declaration: {@link withDeclaredProtocolVersion} stamps the version this\n * build speaks (`DEEP_AGENT_PROTOCOL_VERSION`, generated from the Python\n * source of truth) onto the attach headers — they ride the first WS frame's\n * mandatory `headers` field / the SSE request headers, so no statewire\n * change is needed. The host records it for attach telemetry and\n * per-version behaviour selection only.\n * - Advertisement: the host publishes its own `version` under the `protocol`\n * state key in every attach snapshot; {@link readDeepAgentProtocol} /\n * {@link projectDeepAgentProtocol} read it.\n *\n * There is no minimum client version: the host refuses no command for the\n * version its client declared (or for declaring none), so nothing here\n * compares versions or gates a send. Hosts still publish a `minClientVersion`\n * key (the constant 1) for installed clients whose lane reader required it;\n * this reader ignores it.\n *\n * Skew-tolerant on both sides: an old server advertises nothing (project\n * `null`, gate nothing), and a malformed lane reads as absent — the\n * declaration must never brick a client that could otherwise talk.\n */\nexport interface DeepAgentWireProtocol {\n /** The wire-contract version the server speaks. */\n version: number;\n}\n\n/** The advertised-lane projection a host surface reads. */\nexport interface DeepAgentProtocolStatus {\n /** Server-advertised current contract version. */\n protocolVersion: number;\n /** The contract version THIS client build declares. */\n clientVersion: number;\n}\n\nconst isContractVersion = (value: unknown): value is number =>\n typeof value === 'number' && Number.isInteger(value) && value >= 1;\n\n/**\n * The server-advertised version lane off a raw statewire state, or `null`\n * while it is unreadable — pre-attach, a pre-negotiation server's snapshot\n * (no lane), or a malformed lane (tolerated as absent, never thrown on).\n */\nexport function readDeepAgentProtocol(state: unknown): DeepAgentWireProtocol | null {\n if (typeof state !== 'object' || state === null) return null;\n const lane = (state as Record<string, unknown>)[DEEP_AGENT_PROTOCOL_STATE_KEY];\n if (typeof lane !== 'object' || lane === null) return null;\n const { version } = lane as Record<string, unknown>;\n if (!isContractVersion(version)) return null;\n return { version };\n}\n\n/**\n * Project the advertised version for a host surface, or `null` while the\n * server has not advertised (see {@link readDeepAgentProtocol}). Pure and\n * selector-safe, mirroring `readSessionPersisted`.\n */\nexport function projectDeepAgentProtocol(state: unknown): DeepAgentProtocolStatus | null {\n const lane = readDeepAgentProtocol(state);\n if (lane === null) return null;\n return {\n protocolVersion: lane.version,\n clientVersion: DEEP_AGENT_PROTOCOL_VERSION,\n };\n}\n\n/**\n * The protocol lanes seeds carry ({@link withSeedProtocolLane}), by identity.\n * Statewire's optimistic folds are copy-on-write, so every state folded over a\n * seed keeps its lane object, while a state the host sent never holds one\n * (each is parsed off the wire).\n */\nconst seedProtocolLanes = new WeakSet<object>();\n\n/**\n * `state` with a protocol lane of its own, remembered as a seed's — for the\n * remembered states a mount is seeded with (`toThreadSnapshotSeed`), whose\n * lanes are what the host sent an earlier attach. The lane is copied rather\n * than remembered in place, so a live attach still holding the original object\n * is never read as a seed.\n */\nexport function withSeedProtocolLane(state: DeepAgentStreamValues): DeepAgentStreamValues {\n const lane = state.protocol;\n if (typeof lane !== 'object' || lane === null) return state;\n const seeded = { ...lane };\n seedProtocolLanes.add(seeded);\n return { ...state, protocol: seeded };\n}\n\n/**\n * Whether `state` is one the host sent on this attach. The host advertises the\n * protocol lane in every attach snapshot and keeps it on every later frame;\n * the states a thread renders before its snapshot carry none (the placeholder\n * an existing thread loads with, and any optimistic fold over it) or a seed's\n * ({@link withSeedProtocolLane}).\n *\n * Decide anything about what the host holds — its config lane above all —\n * from these states only, never from the connection status: on a thread\n * mid-run the channel reports `connected` before the attach snapshot applies,\n * and a placeholder read as the host's says the thread has no config.\n *\n * A host predating the lane (2026-08-28) never answers true; the config lane\n * writes clients make (`config/patch`, 2026-09-05) came after it.\n */\nexport function isDeepAgentHostState(state: unknown): boolean {\n if (typeof state !== 'object' || state === null) return false;\n const lane = (state as Record<string, unknown>)[DEEP_AGENT_PROTOCOL_STATE_KEY];\n return (\n typeof lane === 'object' &&\n lane !== null &&\n readDeepAgentProtocol(state) !== null &&\n !seedProtocolLanes.has(lane)\n );\n}\n\n/**\n * Attach headers with this build's contract version declared. Applied by\n * `useDeepAgentThread` to every resolved header set, so all surfaces declare\n * without per-host wiring; an explicit entry (tests, forced overrides) wins.\n */\nexport function withDeclaredProtocolVersion(\n headers: Record<string, string>\n): Record<string, string> {\n if (DEEP_AGENT_PROTOCOL_VERSION_HEADER in headers) return headers;\n return {\n ...headers,\n [DEEP_AGENT_PROTOCOL_VERSION_HEADER]: String(DEEP_AGENT_PROTOCOL_VERSION),\n };\n}\n"],"names":[],"mappings":";AA8CA,MAAM,oBAAoB,CAAC,UACzB,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS;AAO5D,SAAS,sBAAsB,OAA8C;AAClF,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,OAAQ,MAAkC,6BAA6B;AAC7E,MAAI,OAAO,SAAS,YAAY,SAAS,KAAM,QAAO;AACtD,QAAM,EAAE,YAAY;AACpB,MAAI,CAAC,kBAAkB,OAAO,EAAG,QAAO;AACxC,SAAO,EAAE,QAAA;AACX;AAOO,SAAS,yBAAyB,OAAgD;AACvF,QAAM,OAAO,sBAAsB,KAAK;AACxC,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO;AAAA,IACL,iBAAiB,KAAK;AAAA,IACtB,eAAe;AAAA,EAAA;AAEnB;AAQA,MAAM,wCAAwB,QAAA;AASvB,SAAS,qBAAqB,OAAqD;AACxF,QAAM,OAAO,MAAM;AACnB,MAAI,OAAO,SAAS,YAAY,SAAS,KAAM,QAAO;AACtD,QAAM,SAAS,EAAE,GAAG,KAAA;AACpB,oBAAkB,IAAI,MAAM;AAC5B,SAAO,EAAE,GAAG,OAAO,UAAU,OAAA;AAC/B;AAiBO,SAAS,qBAAqB,OAAyB;AAC5D,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,OAAQ,MAAkC,6BAA6B;AAC7E,SACE,OAAO,SAAS,YAChB,SAAS,QACT,sBAAsB,KAAK,MAAM,QACjC,CAAC,kBAAkB,IAAI,IAAI;AAE/B;AAOO,SAAS,4BACd,SACwB;AACxB,MAAI,sCAAsC,QAAS,QAAO;AAC1D,SAAO;AAAA,IACL,GAAG;AAAA,IACH,CAAC,kCAAkC,GAAG,OAAO,2BAA2B;AAAA,EAAA;AAE5E;"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const protocol = require("./protocol.cjs");
|
|
3
4
|
const DEFAULT_MAX_THREADS = 30;
|
|
4
5
|
const DEFAULT_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
5
6
|
function createDeepAgentThreadSnapshotCache(options = {}) {
|
|
@@ -70,7 +71,7 @@ function toThreadSnapshotSeed(state) {
|
|
|
70
71
|
outOfBandRunActive: _outOfBandRunActive,
|
|
71
72
|
...rest
|
|
72
73
|
} = state;
|
|
73
|
-
return { ...rest, runs: seedRunsLane(state.runs) };
|
|
74
|
+
return protocol.withSeedProtocolLane({ ...rest, runs: seedRunsLane(state.runs) });
|
|
74
75
|
}
|
|
75
76
|
function seedRunsLane(runs) {
|
|
76
77
|
const live = runs == null ? void 0 : runs[0];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread-snapshot-cache.cjs","sources":["../../../../agent-runtime/src/thread-snapshot-cache.ts"],"sourcesContent":["import type { DeepAgentStreamValues } from './types';\n\n/**\n * Per-tab LRU of the last wire state seen per thread, so a switch back to a\n * recently-visited thread can mount with that snapshot instead of an empty\n * \"loading\" state (see `UseDeepAgentThreadOptions.snapshotCache`). Entries are\n * references into states the wire already produced — remembering is O(1) and\n * copies nothing; the bound is `maxThreads` retained states.\n */\nexport interface DeepAgentThreadSnapshotCache {\n /** Record the latest hydrated wire state for a thread. */\n remember(threadId: string, state: DeepAgentStreamValues): void;\n /** The freshest remembered state for a thread, or `undefined` when none is\n * held or the entry aged out. */\n take(threadId: string): DeepAgentStreamValues | undefined;\n /**\n * Drop a single thread's entry. Called when the server refuses\n * authorization for the thread — a refused thread must not warm-paint on\n * its next mount.\n */\n forget(threadId: string): void;\n /**\n * Drop every entry. Called when the acting identity changes — a snapshot\n * captured under one account must never seed a mount under another.\n */\n clear(): void;\n /**\n * Whether a fresh entry is held for the thread, without consuming or\n * touching it. Hosts read it at selection time to decide whether the switch\n * can paint from a seed (no loading treatment) or must cover the attach\n * wait. Optional so hand-rolled caches keep type-checking; the built-in\n * cache implements it.\n */\n peek?(threadId: string): boolean;\n}\n\n/** A write observed on the cache — the durable-mirror seam (see `onChange`). */\nexport type DeepAgentThreadSnapshotCacheChange =\n | { type: 'remember'; threadId: string; state: DeepAgentStreamValues; at: number }\n | { type: 'forget'; threadId: string }\n | { type: 'clear' };\n\nexport interface DeepAgentThreadSnapshotCacheOptions {\n /** Threads retained; the least-recently written entry evicts beyond this. */\n maxThreads?: number;\n /**\n * Entries older than this are dropped at read time. The seed's value is\n * \"the thread you just left\" — a snapshot from long ago is more likely to\n * flash content the server has since rewritten than to save a paint.\n */\n maxAgeMs?: number;\n /**\n * Observer of every write (`remember`, `forget`, `clear`). Hosts mirror\n * entries into durable storage from here so a reload can seed too; the\n * observer is never invoked for `restore`, which re-admits mirrored entries.\n * Exceptions are swallowed — a mirror fault must not break the wire path.\n */\n onChange?: (change: DeepAgentThreadSnapshotCacheChange) => void;\n /** Injectable clock (tests). */\n now?: () => number;\n}\n\n/** The built-in cache: the host contract plus the durable-mirror re-admission. */\nexport interface CreatedDeepAgentThreadSnapshotCache extends DeepAgentThreadSnapshotCache {\n peek(threadId: string): boolean;\n /**\n * Re-admit an entry captured earlier — a mirrored snapshot after a reload —\n * under its ORIGINAL capture time, so the age bound still applies. Never\n * displaces a fresher in-memory entry and never notifies `onChange`.\n */\n restore(threadId: string, state: DeepAgentStreamValues, at: number): void;\n}\n\nconst DEFAULT_MAX_THREADS = 30;\nconst DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;\n\nexport function createDeepAgentThreadSnapshotCache(\n options: DeepAgentThreadSnapshotCacheOptions = {}\n): CreatedDeepAgentThreadSnapshotCache {\n const maxThreads = options.maxThreads ?? DEFAULT_MAX_THREADS;\n const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;\n const now = options.now ?? (() => Date.now());\n const notify = (change: DeepAgentThreadSnapshotCacheChange): void => {\n try {\n options.onChange?.(change);\n } catch {\n // The mirror is best-effort; the in-memory cache stays authoritative.\n }\n };\n // Map iteration order is insertion order; delete-then-set on every write\n // keeps the first key the least-recently written.\n const entries = new Map<string, { state: DeepAgentStreamValues; at: number }>();\n const evict = (): void => {\n while (entries.size > maxThreads) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n };\n const fresh = (threadId: string): { state: DeepAgentStreamValues; at: number } | undefined => {\n const entry = entries.get(threadId);\n if (!entry) return undefined;\n if (now() - entry.at > maxAgeMs) {\n entries.delete(threadId);\n return undefined;\n }\n return entry;\n };\n return {\n remember(threadId, state) {\n const at = now();\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n notify({ type: 'remember', threadId, state, at });\n },\n take(threadId) {\n return fresh(threadId)?.state;\n },\n peek(threadId) {\n return fresh(threadId) !== undefined;\n },\n restore(threadId, state, at) {\n if (now() - at > maxAgeMs) return;\n const existing = entries.get(threadId);\n if (existing && existing.at >= at) return;\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n },\n forget(threadId) {\n entries.delete(threadId);\n notify({ type: 'forget', threadId });\n },\n clear() {\n entries.clear();\n notify({ type: 'clear' });\n },\n };\n}\n\n/**\n * Shape a remembered wire state for use as a mount's `initialState`.\n *\n * Messages/todos and the other app-owned keys seed as captured. The run lane\n * seeds by what the user left: a thread whose run was `running` when the\n * snapshot was taken seeds a running entry (see {@link seedRunsLane}) so the\n * switch back reads Running with a Stop control on its first frame — the same\n * frame `runs: []` would render as \"ready\" with a Send button over a thread the\n * server is still mid-run on. Every other live-lifecycle lane normalizes to\n * resting: a seeded input request could collect a decision the server may have\n * already resolved, so an `input-required` entry seeds as no run at all.\n * `__interrupt__` is dropped for the same reason, and so are `ptc_subagents`\n * (the live in-eval dispatch lane — a seeded \"running\" dispatch card under a\n * resting thread would render active with no eval behind it) and\n * `outOfBandRunActive` (the host's out-of-band-run-in-progress flag — seeding a\n * finished run's `true` would hide the composer until the attach corrected\n * it). Durable thread properties (`runConfig`, `sessionPersisted`, …) seed as\n * captured; the attach snapshot then replaces the whole seed.\n */\nexport function toThreadSnapshotSeed(state: DeepAgentStreamValues): DeepAgentStreamValues {\n const {\n __interrupt__: _interrupt,\n ptc_subagents: _ptcSubagents,\n outOfBandRunActive: _outOfBandRunActive,\n ...rest\n } = state;\n return { ...rest, runs: seedRunsLane(state.runs) };\n}\n\n/**\n * The run entries a seed carries.\n *\n * Only a `running` entry survives, and only its replicated identity and its\n * durable lanes: `runId` (the target of a Stop sent during the attach\n * round-trip — the live run's id if it is still running, and a refused stop\n * if it finished meanwhile), `epoch` (rides that Stop so a replayed stop cannot\n * end a later leg) and the `queue`/`steerQueue` entries (server lanes the tray\n * renders). The in-flight records rest until the snapshot: `dispatching` and\n * `nextDispatch` are batches the materializer would re-apply onto messages\n * that already contain them, `stopping` is a rewind the host may have\n * finished, `inputRequests` is a parked decision, and `error` is a failure the\n * next user run clears. An entry in any other status — `stopped`, `error`,\n * `input-required` — seeds as no run.\n */\nfunction seedRunsLane(\n runs: DeepAgentStreamValues['runs']\n): NonNullable<DeepAgentStreamValues['runs']> {\n const live = runs?.[0];\n if (live === undefined || live.status !== 'running') return [];\n return [\n {\n runId: live.runId,\n status: 'running',\n queue: live.queue,\n steerQueue: live.steerQueue,\n ...(live.epoch !== undefined && { epoch: live.epoch }),\n },\n ];\n}\n"],"names":[],"mappings":";;AAyEA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB,KAAK,KAAK;AAE9B,SAAS,mCACd,UAA+C,IACV;AACrC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAA;AACvC,QAAM,SAAS,CAAC,WAAqD;;AACnE,QAAI;AACF,oBAAQ,aAAR,iCAAmB;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,8BAAc,IAAA;AACpB,QAAM,QAAQ,MAAY;AACxB,WAAO,QAAQ,OAAO,YAAY;AAChC,YAAM,SAAS,QAAQ,KAAA,EAAO,OAAO;AACrC,UAAI,WAAW,OAAW;AAC1B,cAAQ,OAAO,MAAM;AAAA,IACvB;AAAA,EACF;AACA,QAAM,QAAQ,CAAC,aAA+E;AAC5F,UAAM,QAAQ,QAAQ,IAAI,QAAQ;AAClC,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,IAAA,IAAQ,MAAM,KAAK,UAAU;AAC/B,cAAQ,OAAO,QAAQ;AACvB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,SAAS,UAAU,OAAO;AACxB,YAAM,KAAK,IAAA;AACX,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AACA,aAAO,EAAE,MAAM,YAAY,UAAU,OAAO,IAAI;AAAA,IAClD;AAAA,IACA,KAAK,UAAU;;AACb,cAAO,WAAM,QAAQ,MAAd,mBAAiB;AAAA,IAC1B;AAAA,IACA,KAAK,UAAU;AACb,aAAO,MAAM,QAAQ,MAAM;AAAA,IAC7B;AAAA,IACA,QAAQ,UAAU,OAAO,IAAI;AAC3B,UAAI,IAAA,IAAQ,KAAK,SAAU;AAC3B,YAAM,WAAW,QAAQ,IAAI,QAAQ;AACrC,UAAI,YAAY,SAAS,MAAM,GAAI;AACnC,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AAAA,IACF;AAAA,IACA,OAAO,UAAU;AACf,cAAQ,OAAO,QAAQ;AACvB,aAAO,EAAE,MAAM,UAAU,SAAA,CAAU;AAAA,IACrC;AAAA,IACA,QAAQ;AACN,cAAQ,MAAA;AACR,aAAO,EAAE,MAAM,SAAS;AAAA,IAC1B;AAAA,EAAA;AAEJ;AAqBO,SAAS,qBAAqB,OAAqD;AACxF,QAAM;AAAA,IACJ,eAAe;AAAA,IACf,eAAe;AAAA,IACf,oBAAoB;AAAA,IACpB,GAAG;AAAA,EAAA,IACD;AACJ,SAAO,EAAE,GAAG,MAAM,MAAM,aAAa,MAAM,IAAI,EAAA;AACjD;AAiBA,SAAS,aACP,MAC4C;AAC5C,QAAM,OAAO,6BAAO;AACpB,MAAI,SAAS,UAAa,KAAK,WAAW,kBAAkB,CAAA;AAC5D,SAAO;AAAA,IACL;AAAA,MACE,OAAO,KAAK;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO,KAAK;AAAA,MACZ,YAAY,KAAK;AAAA,MACjB,GAAI,KAAK,UAAU,UAAa,EAAE,OAAO,KAAK,MAAA;AAAA,IAAM;AAAA,EACtD;AAEJ;;;"}
|
|
1
|
+
{"version":3,"file":"thread-snapshot-cache.cjs","sources":["../../../../agent-runtime/src/thread-snapshot-cache.ts"],"sourcesContent":["import { withSeedProtocolLane } from './protocol';\nimport type { DeepAgentStreamValues } from './types';\n\n/**\n * Per-tab LRU of the last wire state seen per thread, so a switch back to a\n * recently-visited thread can mount with that snapshot instead of an empty\n * \"loading\" state (see `UseDeepAgentThreadOptions.snapshotCache`). Entries are\n * references into states the wire already produced — remembering is O(1) and\n * copies nothing; the bound is `maxThreads` retained states.\n */\nexport interface DeepAgentThreadSnapshotCache {\n /** Record the latest hydrated wire state for a thread. */\n remember(threadId: string, state: DeepAgentStreamValues): void;\n /** The freshest remembered state for a thread, or `undefined` when none is\n * held or the entry aged out. */\n take(threadId: string): DeepAgentStreamValues | undefined;\n /**\n * Drop a single thread's entry. Called when the server refuses\n * authorization for the thread — a refused thread must not warm-paint on\n * its next mount.\n */\n forget(threadId: string): void;\n /**\n * Drop every entry. Called when the acting identity changes — a snapshot\n * captured under one account must never seed a mount under another.\n */\n clear(): void;\n /**\n * Whether a fresh entry is held for the thread, without consuming or\n * touching it. Hosts read it at selection time to decide whether the switch\n * can paint from a seed (no loading treatment) or must cover the attach\n * wait. Optional so hand-rolled caches keep type-checking; the built-in\n * cache implements it.\n */\n peek?(threadId: string): boolean;\n}\n\n/** A write observed on the cache — the durable-mirror seam (see `onChange`). */\nexport type DeepAgentThreadSnapshotCacheChange =\n | { type: 'remember'; threadId: string; state: DeepAgentStreamValues; at: number }\n | { type: 'forget'; threadId: string }\n | { type: 'clear' };\n\nexport interface DeepAgentThreadSnapshotCacheOptions {\n /** Threads retained; the least-recently written entry evicts beyond this. */\n maxThreads?: number;\n /**\n * Entries older than this are dropped at read time. The seed's value is\n * \"the thread you just left\" — a snapshot from long ago is more likely to\n * flash content the server has since rewritten than to save a paint.\n */\n maxAgeMs?: number;\n /**\n * Observer of every write (`remember`, `forget`, `clear`). Hosts mirror\n * entries into durable storage from here so a reload can seed too; the\n * observer is never invoked for `restore`, which re-admits mirrored entries.\n * Exceptions are swallowed — a mirror fault must not break the wire path.\n */\n onChange?: (change: DeepAgentThreadSnapshotCacheChange) => void;\n /** Injectable clock (tests). */\n now?: () => number;\n}\n\n/** The built-in cache: the host contract plus the durable-mirror re-admission. */\nexport interface CreatedDeepAgentThreadSnapshotCache extends DeepAgentThreadSnapshotCache {\n peek(threadId: string): boolean;\n /**\n * Re-admit an entry captured earlier — a mirrored snapshot after a reload —\n * under its ORIGINAL capture time, so the age bound still applies. Never\n * displaces a fresher in-memory entry and never notifies `onChange`.\n */\n restore(threadId: string, state: DeepAgentStreamValues, at: number): void;\n}\n\nconst DEFAULT_MAX_THREADS = 30;\nconst DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;\n\nexport function createDeepAgentThreadSnapshotCache(\n options: DeepAgentThreadSnapshotCacheOptions = {}\n): CreatedDeepAgentThreadSnapshotCache {\n const maxThreads = options.maxThreads ?? DEFAULT_MAX_THREADS;\n const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;\n const now = options.now ?? (() => Date.now());\n const notify = (change: DeepAgentThreadSnapshotCacheChange): void => {\n try {\n options.onChange?.(change);\n } catch {\n // The mirror is best-effort; the in-memory cache stays authoritative.\n }\n };\n // Map iteration order is insertion order; delete-then-set on every write\n // keeps the first key the least-recently written.\n const entries = new Map<string, { state: DeepAgentStreamValues; at: number }>();\n const evict = (): void => {\n while (entries.size > maxThreads) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n };\n const fresh = (threadId: string): { state: DeepAgentStreamValues; at: number } | undefined => {\n const entry = entries.get(threadId);\n if (!entry) return undefined;\n if (now() - entry.at > maxAgeMs) {\n entries.delete(threadId);\n return undefined;\n }\n return entry;\n };\n return {\n remember(threadId, state) {\n const at = now();\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n notify({ type: 'remember', threadId, state, at });\n },\n take(threadId) {\n return fresh(threadId)?.state;\n },\n peek(threadId) {\n return fresh(threadId) !== undefined;\n },\n restore(threadId, state, at) {\n if (now() - at > maxAgeMs) return;\n const existing = entries.get(threadId);\n if (existing && existing.at >= at) return;\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n },\n forget(threadId) {\n entries.delete(threadId);\n notify({ type: 'forget', threadId });\n },\n clear() {\n entries.clear();\n notify({ type: 'clear' });\n },\n };\n}\n\n/**\n * Shape a remembered wire state for use as a mount's `initialState`.\n *\n * Messages/todos and the other app-owned keys seed as captured. The run lane\n * seeds by what the user left: a thread whose run was `running` when the\n * snapshot was taken seeds a running entry (see {@link seedRunsLane}) so the\n * switch back reads Running with a Stop control on its first frame — the same\n * frame `runs: []` would render as \"ready\" with a Send button over a thread the\n * server is still mid-run on. Every other live-lifecycle lane normalizes to\n * resting: a seeded input request could collect a decision the server may have\n * already resolved, so an `input-required` entry seeds as no run at all.\n * `__interrupt__` is dropped for the same reason, and so are `ptc_subagents`\n * (the live in-eval dispatch lane — a seeded \"running\" dispatch card under a\n * resting thread would render active with no eval behind it) and\n * `outOfBandRunActive` (the host's out-of-band-run-in-progress flag — seeding a\n * finished run's `true` would hide the composer until the attach corrected\n * it). Durable thread properties (`runConfig`, `config`, `sessionPersisted`, …)\n * seed as captured; the attach snapshot then replaces the whole seed. The\n * protocol lane seeds as a seed's own copy (`withSeedProtocolLane`), so\n * `isDeepAgentHostState` never reads the seed, or a state folded over it, as\n * what the host holds now.\n */\nexport function toThreadSnapshotSeed(state: DeepAgentStreamValues): DeepAgentStreamValues {\n const {\n __interrupt__: _interrupt,\n ptc_subagents: _ptcSubagents,\n outOfBandRunActive: _outOfBandRunActive,\n ...rest\n } = state;\n return withSeedProtocolLane({ ...rest, runs: seedRunsLane(state.runs) });\n}\n\n/**\n * The run entries a seed carries.\n *\n * Only a `running` entry survives, and only its replicated identity and its\n * durable lanes: `runId` (the target of a Stop sent during the attach\n * round-trip — the live run's id if it is still running, and a refused stop\n * if it finished meanwhile), `epoch` (rides that Stop so a replayed stop cannot\n * end a later leg) and the `queue`/`steerQueue` entries (server lanes the tray\n * renders). The in-flight records rest until the snapshot: `dispatching` and\n * `nextDispatch` are batches the materializer would re-apply onto messages\n * that already contain them, `stopping` is a rewind the host may have\n * finished, `inputRequests` is a parked decision, and `error` is a failure the\n * next user run clears. An entry in any other status — `stopped`, `error`,\n * `input-required` — seeds as no run.\n */\nfunction seedRunsLane(\n runs: DeepAgentStreamValues['runs']\n): NonNullable<DeepAgentStreamValues['runs']> {\n const live = runs?.[0];\n if (live === undefined || live.status !== 'running') return [];\n return [\n {\n runId: live.runId,\n status: 'running',\n queue: live.queue,\n steerQueue: live.steerQueue,\n ...(live.epoch !== undefined && { epoch: live.epoch }),\n },\n ];\n}\n"],"names":["withSeedProtocolLane"],"mappings":";;;AA0EA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB,KAAK,KAAK;AAE9B,SAAS,mCACd,UAA+C,IACV;AACrC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAA;AACvC,QAAM,SAAS,CAAC,WAAqD;;AACnE,QAAI;AACF,oBAAQ,aAAR,iCAAmB;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,8BAAc,IAAA;AACpB,QAAM,QAAQ,MAAY;AACxB,WAAO,QAAQ,OAAO,YAAY;AAChC,YAAM,SAAS,QAAQ,KAAA,EAAO,OAAO;AACrC,UAAI,WAAW,OAAW;AAC1B,cAAQ,OAAO,MAAM;AAAA,IACvB;AAAA,EACF;AACA,QAAM,QAAQ,CAAC,aAA+E;AAC5F,UAAM,QAAQ,QAAQ,IAAI,QAAQ;AAClC,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,IAAA,IAAQ,MAAM,KAAK,UAAU;AAC/B,cAAQ,OAAO,QAAQ;AACvB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,SAAS,UAAU,OAAO;AACxB,YAAM,KAAK,IAAA;AACX,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AACA,aAAO,EAAE,MAAM,YAAY,UAAU,OAAO,IAAI;AAAA,IAClD;AAAA,IACA,KAAK,UAAU;;AACb,cAAO,WAAM,QAAQ,MAAd,mBAAiB;AAAA,IAC1B;AAAA,IACA,KAAK,UAAU;AACb,aAAO,MAAM,QAAQ,MAAM;AAAA,IAC7B;AAAA,IACA,QAAQ,UAAU,OAAO,IAAI;AAC3B,UAAI,IAAA,IAAQ,KAAK,SAAU;AAC3B,YAAM,WAAW,QAAQ,IAAI,QAAQ;AACrC,UAAI,YAAY,SAAS,MAAM,GAAI;AACnC,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AAAA,IACF;AAAA,IACA,OAAO,UAAU;AACf,cAAQ,OAAO,QAAQ;AACvB,aAAO,EAAE,MAAM,UAAU,SAAA,CAAU;AAAA,IACrC;AAAA,IACA,QAAQ;AACN,cAAQ,MAAA;AACR,aAAO,EAAE,MAAM,SAAS;AAAA,IAC1B;AAAA,EAAA;AAEJ;AAwBO,SAAS,qBAAqB,OAAqD;AACxF,QAAM;AAAA,IACJ,eAAe;AAAA,IACf,eAAe;AAAA,IACf,oBAAoB;AAAA,IACpB,GAAG;AAAA,EAAA,IACD;AACJ,SAAOA,SAAAA,qBAAqB,EAAE,GAAG,MAAM,MAAM,aAAa,MAAM,IAAI,GAAG;AACzE;AAiBA,SAAS,aACP,MAC4C;AAC5C,QAAM,OAAO,6BAAO;AACpB,MAAI,SAAS,UAAa,KAAK,WAAW,kBAAkB,CAAA;AAC5D,SAAO;AAAA,IACL;AAAA,MACE,OAAO,KAAK;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO,KAAK;AAAA,MACZ,YAAY,KAAK;AAAA,MACjB,GAAI,KAAK,UAAU,UAAa,EAAE,OAAO,KAAK,MAAA;AAAA,IAAM;AAAA,EACtD;AAEJ;;;"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSeedProtocolLane } from "./protocol.js";
|
|
1
2
|
const DEFAULT_MAX_THREADS = 30;
|
|
2
3
|
const DEFAULT_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
3
4
|
function createDeepAgentThreadSnapshotCache(options = {}) {
|
|
@@ -68,7 +69,7 @@ function toThreadSnapshotSeed(state) {
|
|
|
68
69
|
outOfBandRunActive: _outOfBandRunActive,
|
|
69
70
|
...rest
|
|
70
71
|
} = state;
|
|
71
|
-
return { ...rest, runs: seedRunsLane(state.runs) };
|
|
72
|
+
return withSeedProtocolLane({ ...rest, runs: seedRunsLane(state.runs) });
|
|
72
73
|
}
|
|
73
74
|
function seedRunsLane(runs) {
|
|
74
75
|
const live = runs == null ? void 0 : runs[0];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread-snapshot-cache.js","sources":["../../../../agent-runtime/src/thread-snapshot-cache.ts"],"sourcesContent":["import type { DeepAgentStreamValues } from './types';\n\n/**\n * Per-tab LRU of the last wire state seen per thread, so a switch back to a\n * recently-visited thread can mount with that snapshot instead of an empty\n * \"loading\" state (see `UseDeepAgentThreadOptions.snapshotCache`). Entries are\n * references into states the wire already produced — remembering is O(1) and\n * copies nothing; the bound is `maxThreads` retained states.\n */\nexport interface DeepAgentThreadSnapshotCache {\n /** Record the latest hydrated wire state for a thread. */\n remember(threadId: string, state: DeepAgentStreamValues): void;\n /** The freshest remembered state for a thread, or `undefined` when none is\n * held or the entry aged out. */\n take(threadId: string): DeepAgentStreamValues | undefined;\n /**\n * Drop a single thread's entry. Called when the server refuses\n * authorization for the thread — a refused thread must not warm-paint on\n * its next mount.\n */\n forget(threadId: string): void;\n /**\n * Drop every entry. Called when the acting identity changes — a snapshot\n * captured under one account must never seed a mount under another.\n */\n clear(): void;\n /**\n * Whether a fresh entry is held for the thread, without consuming or\n * touching it. Hosts read it at selection time to decide whether the switch\n * can paint from a seed (no loading treatment) or must cover the attach\n * wait. Optional so hand-rolled caches keep type-checking; the built-in\n * cache implements it.\n */\n peek?(threadId: string): boolean;\n}\n\n/** A write observed on the cache — the durable-mirror seam (see `onChange`). */\nexport type DeepAgentThreadSnapshotCacheChange =\n | { type: 'remember'; threadId: string; state: DeepAgentStreamValues; at: number }\n | { type: 'forget'; threadId: string }\n | { type: 'clear' };\n\nexport interface DeepAgentThreadSnapshotCacheOptions {\n /** Threads retained; the least-recently written entry evicts beyond this. */\n maxThreads?: number;\n /**\n * Entries older than this are dropped at read time. The seed's value is\n * \"the thread you just left\" — a snapshot from long ago is more likely to\n * flash content the server has since rewritten than to save a paint.\n */\n maxAgeMs?: number;\n /**\n * Observer of every write (`remember`, `forget`, `clear`). Hosts mirror\n * entries into durable storage from here so a reload can seed too; the\n * observer is never invoked for `restore`, which re-admits mirrored entries.\n * Exceptions are swallowed — a mirror fault must not break the wire path.\n */\n onChange?: (change: DeepAgentThreadSnapshotCacheChange) => void;\n /** Injectable clock (tests). */\n now?: () => number;\n}\n\n/** The built-in cache: the host contract plus the durable-mirror re-admission. */\nexport interface CreatedDeepAgentThreadSnapshotCache extends DeepAgentThreadSnapshotCache {\n peek(threadId: string): boolean;\n /**\n * Re-admit an entry captured earlier — a mirrored snapshot after a reload —\n * under its ORIGINAL capture time, so the age bound still applies. Never\n * displaces a fresher in-memory entry and never notifies `onChange`.\n */\n restore(threadId: string, state: DeepAgentStreamValues, at: number): void;\n}\n\nconst DEFAULT_MAX_THREADS = 30;\nconst DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;\n\nexport function createDeepAgentThreadSnapshotCache(\n options: DeepAgentThreadSnapshotCacheOptions = {}\n): CreatedDeepAgentThreadSnapshotCache {\n const maxThreads = options.maxThreads ?? DEFAULT_MAX_THREADS;\n const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;\n const now = options.now ?? (() => Date.now());\n const notify = (change: DeepAgentThreadSnapshotCacheChange): void => {\n try {\n options.onChange?.(change);\n } catch {\n // The mirror is best-effort; the in-memory cache stays authoritative.\n }\n };\n // Map iteration order is insertion order; delete-then-set on every write\n // keeps the first key the least-recently written.\n const entries = new Map<string, { state: DeepAgentStreamValues; at: number }>();\n const evict = (): void => {\n while (entries.size > maxThreads) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n };\n const fresh = (threadId: string): { state: DeepAgentStreamValues; at: number } | undefined => {\n const entry = entries.get(threadId);\n if (!entry) return undefined;\n if (now() - entry.at > maxAgeMs) {\n entries.delete(threadId);\n return undefined;\n }\n return entry;\n };\n return {\n remember(threadId, state) {\n const at = now();\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n notify({ type: 'remember', threadId, state, at });\n },\n take(threadId) {\n return fresh(threadId)?.state;\n },\n peek(threadId) {\n return fresh(threadId) !== undefined;\n },\n restore(threadId, state, at) {\n if (now() - at > maxAgeMs) return;\n const existing = entries.get(threadId);\n if (existing && existing.at >= at) return;\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n },\n forget(threadId) {\n entries.delete(threadId);\n notify({ type: 'forget', threadId });\n },\n clear() {\n entries.clear();\n notify({ type: 'clear' });\n },\n };\n}\n\n/**\n * Shape a remembered wire state for use as a mount's `initialState`.\n *\n * Messages/todos and the other app-owned keys seed as captured. The run lane\n * seeds by what the user left: a thread whose run was `running` when the\n * snapshot was taken seeds a running entry (see {@link seedRunsLane}) so the\n * switch back reads Running with a Stop control on its first frame — the same\n * frame `runs: []` would render as \"ready\" with a Send button over a thread the\n * server is still mid-run on. Every other live-lifecycle lane normalizes to\n * resting: a seeded input request could collect a decision the server may have\n * already resolved, so an `input-required` entry seeds as no run at all.\n * `__interrupt__` is dropped for the same reason, and so are `ptc_subagents`\n * (the live in-eval dispatch lane — a seeded \"running\" dispatch card under a\n * resting thread would render active with no eval behind it) and\n * `outOfBandRunActive` (the host's out-of-band-run-in-progress flag — seeding a\n * finished run's `true` would hide the composer until the attach corrected\n * it). Durable thread properties (`runConfig`, `sessionPersisted`, …) seed as\n * captured; the attach snapshot then replaces the whole seed.\n */\nexport function toThreadSnapshotSeed(state: DeepAgentStreamValues): DeepAgentStreamValues {\n const {\n __interrupt__: _interrupt,\n ptc_subagents: _ptcSubagents,\n outOfBandRunActive: _outOfBandRunActive,\n ...rest\n } = state;\n return { ...rest, runs: seedRunsLane(state.runs) };\n}\n\n/**\n * The run entries a seed carries.\n *\n * Only a `running` entry survives, and only its replicated identity and its\n * durable lanes: `runId` (the target of a Stop sent during the attach\n * round-trip — the live run's id if it is still running, and a refused stop\n * if it finished meanwhile), `epoch` (rides that Stop so a replayed stop cannot\n * end a later leg) and the `queue`/`steerQueue` entries (server lanes the tray\n * renders). The in-flight records rest until the snapshot: `dispatching` and\n * `nextDispatch` are batches the materializer would re-apply onto messages\n * that already contain them, `stopping` is a rewind the host may have\n * finished, `inputRequests` is a parked decision, and `error` is a failure the\n * next user run clears. An entry in any other status — `stopped`, `error`,\n * `input-required` — seeds as no run.\n */\nfunction seedRunsLane(\n runs: DeepAgentStreamValues['runs']\n): NonNullable<DeepAgentStreamValues['runs']> {\n const live = runs?.[0];\n if (live === undefined || live.status !== 'running') return [];\n return [\n {\n runId: live.runId,\n status: 'running',\n queue: live.queue,\n steerQueue: live.steerQueue,\n ...(live.epoch !== undefined && { epoch: live.epoch }),\n },\n ];\n}\n"],"names":[],"mappings":"AAyEA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB,KAAK,KAAK;AAE9B,SAAS,mCACd,UAA+C,IACV;AACrC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAA;AACvC,QAAM,SAAS,CAAC,WAAqD;AATvE;AAUI,QAAI;AACF,oBAAQ,aAAR,iCAAmB;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,8BAAc,IAAA;AACpB,QAAM,QAAQ,MAAY;AACxB,WAAO,QAAQ,OAAO,YAAY;AAChC,YAAM,SAAS,QAAQ,KAAA,EAAO,OAAO;AACrC,UAAI,WAAW,OAAW;AAC1B,cAAQ,OAAO,MAAM;AAAA,IACvB;AAAA,EACF;AACA,QAAM,QAAQ,CAAC,aAA+E;AAC5F,UAAM,QAAQ,QAAQ,IAAI,QAAQ;AAClC,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,IAAA,IAAQ,MAAM,KAAK,UAAU;AAC/B,cAAQ,OAAO,QAAQ;AACvB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,SAAS,UAAU,OAAO;AACxB,YAAM,KAAK,IAAA;AACX,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AACA,aAAO,EAAE,MAAM,YAAY,UAAU,OAAO,IAAI;AAAA,IAClD;AAAA,IACA,KAAK,UAAU;AA3CnB;AA4CM,cAAO,WAAM,QAAQ,MAAd,mBAAiB;AAAA,IAC1B;AAAA,IACA,KAAK,UAAU;AACb,aAAO,MAAM,QAAQ,MAAM;AAAA,IAC7B;AAAA,IACA,QAAQ,UAAU,OAAO,IAAI;AAC3B,UAAI,IAAA,IAAQ,KAAK,SAAU;AAC3B,YAAM,WAAW,QAAQ,IAAI,QAAQ;AACrC,UAAI,YAAY,SAAS,MAAM,GAAI;AACnC,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AAAA,IACF;AAAA,IACA,OAAO,UAAU;AACf,cAAQ,OAAO,QAAQ;AACvB,aAAO,EAAE,MAAM,UAAU,SAAA,CAAU;AAAA,IACrC;AAAA,IACA,QAAQ;AACN,cAAQ,MAAA;AACR,aAAO,EAAE,MAAM,SAAS;AAAA,IAC1B;AAAA,EAAA;AAEJ;AAqBO,SAAS,qBAAqB,OAAqD;AACxF,QAAM;AAAA,IACJ,eAAe;AAAA,IACf,eAAe;AAAA,IACf,oBAAoB;AAAA,IACpB,GAAG;AAAA,EAAA,IACD;AACJ,SAAO,EAAE,GAAG,MAAM,MAAM,aAAa,MAAM,IAAI,EAAA;AACjD;AAiBA,SAAS,aACP,MAC4C;AAC5C,QAAM,OAAO,6BAAO;AACpB,MAAI,SAAS,UAAa,KAAK,WAAW,kBAAkB,CAAA;AAC5D,SAAO;AAAA,IACL;AAAA,MACE,OAAO,KAAK;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO,KAAK;AAAA,MACZ,YAAY,KAAK;AAAA,MACjB,GAAI,KAAK,UAAU,UAAa,EAAE,OAAO,KAAK,MAAA;AAAA,IAAM;AAAA,EACtD;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"thread-snapshot-cache.js","sources":["../../../../agent-runtime/src/thread-snapshot-cache.ts"],"sourcesContent":["import { withSeedProtocolLane } from './protocol';\nimport type { DeepAgentStreamValues } from './types';\n\n/**\n * Per-tab LRU of the last wire state seen per thread, so a switch back to a\n * recently-visited thread can mount with that snapshot instead of an empty\n * \"loading\" state (see `UseDeepAgentThreadOptions.snapshotCache`). Entries are\n * references into states the wire already produced — remembering is O(1) and\n * copies nothing; the bound is `maxThreads` retained states.\n */\nexport interface DeepAgentThreadSnapshotCache {\n /** Record the latest hydrated wire state for a thread. */\n remember(threadId: string, state: DeepAgentStreamValues): void;\n /** The freshest remembered state for a thread, or `undefined` when none is\n * held or the entry aged out. */\n take(threadId: string): DeepAgentStreamValues | undefined;\n /**\n * Drop a single thread's entry. Called when the server refuses\n * authorization for the thread — a refused thread must not warm-paint on\n * its next mount.\n */\n forget(threadId: string): void;\n /**\n * Drop every entry. Called when the acting identity changes — a snapshot\n * captured under one account must never seed a mount under another.\n */\n clear(): void;\n /**\n * Whether a fresh entry is held for the thread, without consuming or\n * touching it. Hosts read it at selection time to decide whether the switch\n * can paint from a seed (no loading treatment) or must cover the attach\n * wait. Optional so hand-rolled caches keep type-checking; the built-in\n * cache implements it.\n */\n peek?(threadId: string): boolean;\n}\n\n/** A write observed on the cache — the durable-mirror seam (see `onChange`). */\nexport type DeepAgentThreadSnapshotCacheChange =\n | { type: 'remember'; threadId: string; state: DeepAgentStreamValues; at: number }\n | { type: 'forget'; threadId: string }\n | { type: 'clear' };\n\nexport interface DeepAgentThreadSnapshotCacheOptions {\n /** Threads retained; the least-recently written entry evicts beyond this. */\n maxThreads?: number;\n /**\n * Entries older than this are dropped at read time. The seed's value is\n * \"the thread you just left\" — a snapshot from long ago is more likely to\n * flash content the server has since rewritten than to save a paint.\n */\n maxAgeMs?: number;\n /**\n * Observer of every write (`remember`, `forget`, `clear`). Hosts mirror\n * entries into durable storage from here so a reload can seed too; the\n * observer is never invoked for `restore`, which re-admits mirrored entries.\n * Exceptions are swallowed — a mirror fault must not break the wire path.\n */\n onChange?: (change: DeepAgentThreadSnapshotCacheChange) => void;\n /** Injectable clock (tests). */\n now?: () => number;\n}\n\n/** The built-in cache: the host contract plus the durable-mirror re-admission. */\nexport interface CreatedDeepAgentThreadSnapshotCache extends DeepAgentThreadSnapshotCache {\n peek(threadId: string): boolean;\n /**\n * Re-admit an entry captured earlier — a mirrored snapshot after a reload —\n * under its ORIGINAL capture time, so the age bound still applies. Never\n * displaces a fresher in-memory entry and never notifies `onChange`.\n */\n restore(threadId: string, state: DeepAgentStreamValues, at: number): void;\n}\n\nconst DEFAULT_MAX_THREADS = 30;\nconst DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;\n\nexport function createDeepAgentThreadSnapshotCache(\n options: DeepAgentThreadSnapshotCacheOptions = {}\n): CreatedDeepAgentThreadSnapshotCache {\n const maxThreads = options.maxThreads ?? DEFAULT_MAX_THREADS;\n const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;\n const now = options.now ?? (() => Date.now());\n const notify = (change: DeepAgentThreadSnapshotCacheChange): void => {\n try {\n options.onChange?.(change);\n } catch {\n // The mirror is best-effort; the in-memory cache stays authoritative.\n }\n };\n // Map iteration order is insertion order; delete-then-set on every write\n // keeps the first key the least-recently written.\n const entries = new Map<string, { state: DeepAgentStreamValues; at: number }>();\n const evict = (): void => {\n while (entries.size > maxThreads) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n };\n const fresh = (threadId: string): { state: DeepAgentStreamValues; at: number } | undefined => {\n const entry = entries.get(threadId);\n if (!entry) return undefined;\n if (now() - entry.at > maxAgeMs) {\n entries.delete(threadId);\n return undefined;\n }\n return entry;\n };\n return {\n remember(threadId, state) {\n const at = now();\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n notify({ type: 'remember', threadId, state, at });\n },\n take(threadId) {\n return fresh(threadId)?.state;\n },\n peek(threadId) {\n return fresh(threadId) !== undefined;\n },\n restore(threadId, state, at) {\n if (now() - at > maxAgeMs) return;\n const existing = entries.get(threadId);\n if (existing && existing.at >= at) return;\n entries.delete(threadId);\n entries.set(threadId, { state, at });\n evict();\n },\n forget(threadId) {\n entries.delete(threadId);\n notify({ type: 'forget', threadId });\n },\n clear() {\n entries.clear();\n notify({ type: 'clear' });\n },\n };\n}\n\n/**\n * Shape a remembered wire state for use as a mount's `initialState`.\n *\n * Messages/todos and the other app-owned keys seed as captured. The run lane\n * seeds by what the user left: a thread whose run was `running` when the\n * snapshot was taken seeds a running entry (see {@link seedRunsLane}) so the\n * switch back reads Running with a Stop control on its first frame — the same\n * frame `runs: []` would render as \"ready\" with a Send button over a thread the\n * server is still mid-run on. Every other live-lifecycle lane normalizes to\n * resting: a seeded input request could collect a decision the server may have\n * already resolved, so an `input-required` entry seeds as no run at all.\n * `__interrupt__` is dropped for the same reason, and so are `ptc_subagents`\n * (the live in-eval dispatch lane — a seeded \"running\" dispatch card under a\n * resting thread would render active with no eval behind it) and\n * `outOfBandRunActive` (the host's out-of-band-run-in-progress flag — seeding a\n * finished run's `true` would hide the composer until the attach corrected\n * it). Durable thread properties (`runConfig`, `config`, `sessionPersisted`, …)\n * seed as captured; the attach snapshot then replaces the whole seed. The\n * protocol lane seeds as a seed's own copy (`withSeedProtocolLane`), so\n * `isDeepAgentHostState` never reads the seed, or a state folded over it, as\n * what the host holds now.\n */\nexport function toThreadSnapshotSeed(state: DeepAgentStreamValues): DeepAgentStreamValues {\n const {\n __interrupt__: _interrupt,\n ptc_subagents: _ptcSubagents,\n outOfBandRunActive: _outOfBandRunActive,\n ...rest\n } = state;\n return withSeedProtocolLane({ ...rest, runs: seedRunsLane(state.runs) });\n}\n\n/**\n * The run entries a seed carries.\n *\n * Only a `running` entry survives, and only its replicated identity and its\n * durable lanes: `runId` (the target of a Stop sent during the attach\n * round-trip — the live run's id if it is still running, and a refused stop\n * if it finished meanwhile), `epoch` (rides that Stop so a replayed stop cannot\n * end a later leg) and the `queue`/`steerQueue` entries (server lanes the tray\n * renders). The in-flight records rest until the snapshot: `dispatching` and\n * `nextDispatch` are batches the materializer would re-apply onto messages\n * that already contain them, `stopping` is a rewind the host may have\n * finished, `inputRequests` is a parked decision, and `error` is a failure the\n * next user run clears. An entry in any other status — `stopped`, `error`,\n * `input-required` — seeds as no run.\n */\nfunction seedRunsLane(\n runs: DeepAgentStreamValues['runs']\n): NonNullable<DeepAgentStreamValues['runs']> {\n const live = runs?.[0];\n if (live === undefined || live.status !== 'running') return [];\n return [\n {\n runId: live.runId,\n status: 'running',\n queue: live.queue,\n steerQueue: live.steerQueue,\n ...(live.epoch !== undefined && { epoch: live.epoch }),\n },\n ];\n}\n"],"names":[],"mappings":";AA0EA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB,KAAK,KAAK;AAE9B,SAAS,mCACd,UAA+C,IACV;AACrC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAA;AACvC,QAAM,SAAS,CAAC,WAAqD;;AACnE,QAAI;AACF,oBAAQ,aAAR,iCAAmB;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,8BAAc,IAAA;AACpB,QAAM,QAAQ,MAAY;AACxB,WAAO,QAAQ,OAAO,YAAY;AAChC,YAAM,SAAS,QAAQ,KAAA,EAAO,OAAO;AACrC,UAAI,WAAW,OAAW;AAC1B,cAAQ,OAAO,MAAM;AAAA,IACvB;AAAA,EACF;AACA,QAAM,QAAQ,CAAC,aAA+E;AAC5F,UAAM,QAAQ,QAAQ,IAAI,QAAQ;AAClC,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,IAAA,IAAQ,MAAM,KAAK,UAAU;AAC/B,cAAQ,OAAO,QAAQ;AACvB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,SAAS,UAAU,OAAO;AACxB,YAAM,KAAK,IAAA;AACX,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AACA,aAAO,EAAE,MAAM,YAAY,UAAU,OAAO,IAAI;AAAA,IAClD;AAAA,IACA,KAAK,UAAU;;AACb,cAAO,WAAM,QAAQ,MAAd,mBAAiB;AAAA,IAC1B;AAAA,IACA,KAAK,UAAU;AACb,aAAO,MAAM,QAAQ,MAAM;AAAA,IAC7B;AAAA,IACA,QAAQ,UAAU,OAAO,IAAI;AAC3B,UAAI,IAAA,IAAQ,KAAK,SAAU;AAC3B,YAAM,WAAW,QAAQ,IAAI,QAAQ;AACrC,UAAI,YAAY,SAAS,MAAM,GAAI;AACnC,cAAQ,OAAO,QAAQ;AACvB,cAAQ,IAAI,UAAU,EAAE,OAAO,IAAI;AACnC,YAAA;AAAA,IACF;AAAA,IACA,OAAO,UAAU;AACf,cAAQ,OAAO,QAAQ;AACvB,aAAO,EAAE,MAAM,UAAU,SAAA,CAAU;AAAA,IACrC;AAAA,IACA,QAAQ;AACN,cAAQ,MAAA;AACR,aAAO,EAAE,MAAM,SAAS;AAAA,IAC1B;AAAA,EAAA;AAEJ;AAwBO,SAAS,qBAAqB,OAAqD;AACxF,QAAM;AAAA,IACJ,eAAe;AAAA,IACf,eAAe;AAAA,IACf,oBAAoB;AAAA,IACpB,GAAG;AAAA,EAAA,IACD;AACJ,SAAO,qBAAqB,EAAE,GAAG,MAAM,MAAM,aAAa,MAAM,IAAI,GAAG;AACzE;AAiBA,SAAS,aACP,MAC4C;AAC5C,QAAM,OAAO,6BAAO;AACpB,MAAI,SAAS,UAAa,KAAK,WAAW,kBAAkB,CAAA;AAC5D,SAAO;AAAA,IACL;AAAA,MACE,OAAO,KAAK;AAAA,MACZ,QAAQ;AAAA,MACR,OAAO,KAAK;AAAA,MACZ,YAAY,KAAK;AAAA,MACjB,GAAI,KAAK,UAAU,UAAa,EAAE,OAAO,KAAK,MAAA;AAAA,IAAM;AAAA,EACtD;AAEJ;"}
|
|
@@ -6,7 +6,12 @@ function createOlympusAssetUrl({
|
|
|
6
6
|
backendUrl,
|
|
7
7
|
assetIds
|
|
8
8
|
}) {
|
|
9
|
-
const
|
|
9
|
+
const configuredAppUrl = appUrl == null ? void 0 : appUrl.trim();
|
|
10
|
+
const resolvedAppUrl = configuredAppUrl ? athenaUrls.athenaLinkAppUrl({
|
|
11
|
+
appUrl: configuredAppUrl,
|
|
12
|
+
backendUrl: backendUrl ?? "",
|
|
13
|
+
pageOrigin: typeof window === "undefined" ? null : window.location.origin
|
|
14
|
+
}) : backendUrl ? athenaUrls.deriveAthenaAppUrl({ backendUrl }) : null;
|
|
10
15
|
if (!resolvedAppUrl) return null;
|
|
11
16
|
const url = athenaUrls.createAthenaSpacesUrl({
|
|
12
17
|
appUrl: resolvedAppUrl,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-urls.cjs","sources":["../../../src/chat-ui/lib/asset-urls.ts"],"sourcesContent":["import {\n createAthenaSpacesUrl,\n deriveAthenaAppUrl,\n} from \"../../lib/athena-urls\";\n\n
|
|
1
|
+
{"version":3,"file":"asset-urls.cjs","sources":["../../../src/chat-ui/lib/asset-urls.ts"],"sourcesContent":["import {\n athenaLinkAppUrl,\n createAthenaSpacesUrl,\n deriveAthenaAppUrl,\n} from \"../../lib/athena-urls\";\n\n/**\n * Deep link into Spaces for one or more assets referenced from chat. In an app\n * a computer serves, the provider's `appUrl` is the app's own origin (Marathon\n * mints it so), and the link goes to the deployment's Athena frontend instead,\n * as the workbench's \"Open in Athena\" links do (`athenaLinkAppUrl`).\n */\nexport function createOlympusAssetUrl({\n appUrl,\n backendUrl,\n assetIds,\n}: {\n appUrl?: string | null;\n backendUrl?: string | null;\n assetIds: string | string[];\n}): string | null {\n const configuredAppUrl = appUrl?.trim();\n const resolvedAppUrl: string | null = configuredAppUrl\n ? athenaLinkAppUrl({\n appUrl: configuredAppUrl,\n backendUrl: backendUrl ?? \"\",\n pageOrigin: typeof window === \"undefined\" ? null : window.location.origin,\n })\n : backendUrl\n ? deriveAthenaAppUrl({ backendUrl })\n : null;\n if (!resolvedAppUrl) return null;\n\n const url = createAthenaSpacesUrl({\n appUrl: resolvedAppUrl,\n assetIds,\n });\n return new URL(url).toString();\n}\n"],"names":["athenaLinkAppUrl","deriveAthenaAppUrl","createAthenaSpacesUrl"],"mappings":";;;AAYO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF,GAIkB;AAChB,QAAM,mBAAmB,iCAAQ;AACjC,QAAM,iBAAgC,mBAClCA,4BAAiB;AAAA,IACf,QAAQ;AAAA,IACR,YAAY,cAAc;AAAA,IAC1B,YAAY,OAAO,WAAW,cAAc,OAAO,OAAO,SAAS;AAAA,EAAA,CACpE,IACD,aACEC,WAAAA,mBAAmB,EAAE,WAAA,CAAY,IACjC;AACN,MAAI,CAAC,eAAgB,QAAO;AAE5B,QAAM,MAAMC,WAAAA,sBAAsB;AAAA,IAChC,QAAQ;AAAA,IACR;AAAA,EAAA,CACD;AACD,SAAO,IAAI,IAAI,GAAG,EAAE,SAAA;AACtB;;"}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Deep link into Spaces for one or more assets referenced from chat. In an app
|
|
3
|
+
* a computer serves, the provider's `appUrl` is the app's own origin (Marathon
|
|
4
|
+
* mints it so), and the link goes to the deployment's Athena frontend instead,
|
|
5
|
+
* as the workbench's "Open in Athena" links do (`athenaLinkAppUrl`).
|
|
6
|
+
*/
|
|
2
7
|
export declare function createOlympusAssetUrl({ appUrl, backendUrl, assetIds, }: {
|
|
3
8
|
appUrl?: string | null;
|
|
4
9
|
backendUrl?: string | null;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { deriveAthenaAppUrl, createAthenaSpacesUrl } from "../../lib/athena-urls.js";
|
|
1
|
+
import { athenaLinkAppUrl, deriveAthenaAppUrl, createAthenaSpacesUrl } from "../../lib/athena-urls.js";
|
|
2
2
|
function createOlympusAssetUrl({
|
|
3
3
|
appUrl,
|
|
4
4
|
backendUrl,
|
|
5
5
|
assetIds
|
|
6
6
|
}) {
|
|
7
|
-
const
|
|
7
|
+
const configuredAppUrl = appUrl == null ? void 0 : appUrl.trim();
|
|
8
|
+
const resolvedAppUrl = configuredAppUrl ? athenaLinkAppUrl({
|
|
9
|
+
appUrl: configuredAppUrl,
|
|
10
|
+
backendUrl: backendUrl ?? "",
|
|
11
|
+
pageOrigin: typeof window === "undefined" ? null : window.location.origin
|
|
12
|
+
}) : backendUrl ? deriveAthenaAppUrl({ backendUrl }) : null;
|
|
8
13
|
if (!resolvedAppUrl) return null;
|
|
9
14
|
const url = createAthenaSpacesUrl({
|
|
10
15
|
appUrl: resolvedAppUrl,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-urls.js","sources":["../../../src/chat-ui/lib/asset-urls.ts"],"sourcesContent":["import {\n createAthenaSpacesUrl,\n deriveAthenaAppUrl,\n} from \"../../lib/athena-urls\";\n\n
|
|
1
|
+
{"version":3,"file":"asset-urls.js","sources":["../../../src/chat-ui/lib/asset-urls.ts"],"sourcesContent":["import {\n athenaLinkAppUrl,\n createAthenaSpacesUrl,\n deriveAthenaAppUrl,\n} from \"../../lib/athena-urls\";\n\n/**\n * Deep link into Spaces for one or more assets referenced from chat. In an app\n * a computer serves, the provider's `appUrl` is the app's own origin (Marathon\n * mints it so), and the link goes to the deployment's Athena frontend instead,\n * as the workbench's \"Open in Athena\" links do (`athenaLinkAppUrl`).\n */\nexport function createOlympusAssetUrl({\n appUrl,\n backendUrl,\n assetIds,\n}: {\n appUrl?: string | null;\n backendUrl?: string | null;\n assetIds: string | string[];\n}): string | null {\n const configuredAppUrl = appUrl?.trim();\n const resolvedAppUrl: string | null = configuredAppUrl\n ? athenaLinkAppUrl({\n appUrl: configuredAppUrl,\n backendUrl: backendUrl ?? \"\",\n pageOrigin: typeof window === \"undefined\" ? null : window.location.origin,\n })\n : backendUrl\n ? deriveAthenaAppUrl({ backendUrl })\n : null;\n if (!resolvedAppUrl) return null;\n\n const url = createAthenaSpacesUrl({\n appUrl: resolvedAppUrl,\n assetIds,\n });\n return new URL(url).toString();\n}\n"],"names":[],"mappings":";AAYO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF,GAIkB;AAChB,QAAM,mBAAmB,iCAAQ;AACjC,QAAM,iBAAgC,mBAClC,iBAAiB;AAAA,IACf,QAAQ;AAAA,IACR,YAAY,cAAc;AAAA,IAC1B,YAAY,OAAO,WAAW,cAAc,OAAO,OAAO,SAAS;AAAA,EAAA,CACpE,IACD,aACE,mBAAmB,EAAE,WAAA,CAAY,IACjC;AACN,MAAI,CAAC,eAAgB,QAAO;AAE5B,QAAM,MAAM,sBAAsB;AAAA,IAChC,QAAQ;AAAA,IACR;AAAA,EAAA,CACD;AACD,SAAO,IAAI,IAAI,GAAG,EAAE,SAAA;AACtB;"}
|
package/dist/index.cjs
CHANGED
|
@@ -330,7 +330,6 @@ exports.SESSION_SANDBOX_LABEL = sessionWorkbenchModel.SESSION_SANDBOX_LABEL;
|
|
|
330
330
|
exports.SHELL_OUTPUT_PREVIEW_CHARS = sessionWorkbenchModel.SHELL_OUTPUT_PREVIEW_CHARS;
|
|
331
331
|
exports.assetPanelTypeOf = sessionWorkbenchModel.assetPanelTypeOf;
|
|
332
332
|
exports.assetTypeLabel = sessionWorkbenchModel.assetTypeLabel;
|
|
333
|
-
exports.athenaLinkAppUrl = sessionWorkbenchModel.athenaLinkAppUrl;
|
|
334
333
|
exports.computerStateBadge = sessionWorkbenchModel.computerStateBadge;
|
|
335
334
|
exports.deriveShellRows = sessionWorkbenchModel.deriveShellRows;
|
|
336
335
|
exports.deriveTaskSteps = sessionWorkbenchModel.deriveTaskSteps;
|
|
@@ -472,6 +471,7 @@ exports.useParentAuth = auth.useParentAuth;
|
|
|
472
471
|
exports.useParentBridge = auth.useParentBridge;
|
|
473
472
|
exports.DEFAULT_APP_URL = athenaUrls.DEFAULT_APP_URL;
|
|
474
473
|
exports.DEFAULT_STATEWIRE_SYNC_URL = athenaUrls.DEFAULT_STATEWIRE_SYNC_URL;
|
|
474
|
+
exports.athenaLinkAppUrl = athenaUrls.athenaLinkAppUrl;
|
|
475
475
|
exports.createAthenaSpacesUrl = athenaUrls.createAthenaSpacesUrl;
|
|
476
476
|
exports.deriveAthenaApiUrl = athenaUrls.deriveAthenaApiUrl;
|
|
477
477
|
exports.deriveAthenaAppUrl = athenaUrls.deriveAthenaAppUrl;
|
package/dist/index.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ export type { SessionWorkbenchProps } from './workbench/SessionWorkbench';
|
|
|
105
105
|
export { SESSION_WORKSPACE_LIVE_REFETCH_MS, useSessionWorkbench, useSessionWorkbenchWorkspace, } from './workbench/session-workbench-context';
|
|
106
106
|
export type { SessionWorkbenchPane, SessionWorkbenchPaneContext, } from './workbench/session-workbench-context';
|
|
107
107
|
export { DEFAULT_SESSION_WORKBENCH_OPEN_PANE_IDS, DEFAULT_SESSION_WORKBENCH_PANES, SESSION_WORKBENCH_ATTACHMENTS_PANE, SESSION_WORKBENCH_COMPUTER_PANE, SESSION_WORKBENCH_PULL_REQUESTS_PANE, SESSION_WORKBENCH_SHELL_PANE, SESSION_WORKBENCH_TASKS_PANE, SessionAttachmentsPane, SessionComputerPane, SessionPullRequestsPane, SessionShellPane, SessionTasksPane, } from './workbench/session-workbench-panes';
|
|
108
|
-
export { assetPanelTypeOf, assetTypeLabel,
|
|
108
|
+
export { assetPanelTypeOf, assetTypeLabel, computerStateBadge, deriveShellRows, deriveTaskSteps, filterShellRows, filterWorkbenchPanes, reviewDecisionLabel, SESSION_SANDBOX_LABEL, SHELL_OUTPUT_PREVIEW_CHARS, shellMachine, taskProgress, toolResultText, workspaceComputers, } from './workbench/session-workbench-model';
|
|
109
109
|
export type { ComputerStateTone, ShellMachine, ShellRow, ShellRowStatus, TaskProgress, WorkbenchMessage, } from './workbench/session-workbench-model';
|
|
110
110
|
export { computerSshAccessPath, computerStatusPath, fetchComputerRuntimeStatus, fetchComputerSshAccess, fetchSessionWorkspace, readComputerRuntimeStatus, readComputerSshAccess, readSessionWorkspace, sessionWorkspacePath, } from './workbench/session-workspace-api';
|
|
111
111
|
export type { ComputerRuntimeStatus, ComputerSshAccess, SessionWorkspace, SessionWorkspaceAsset, SessionWorkspaceRole, } from './workbench/session-workspace-api';
|
|
@@ -204,7 +204,7 @@ export { buildStatewireRunConfig } from './runtime/statewire-run-config';
|
|
|
204
204
|
export type { StatewireRunConfigOptions } from './runtime/statewire-run-config';
|
|
205
205
|
export { useParentAuth, useParentBridge } from './runtime/auth';
|
|
206
206
|
export type { ParentBridge, ParentBridgeState } from './runtime/auth';
|
|
207
|
-
export { createAthenaSpacesUrl, DEFAULT_APP_URL, DEFAULT_STATEWIRE_SYNC_URL, deriveAthenaApiUrl, deriveAthenaAppUrl, deriveStatewireSyncUrl, resolveStatewireSyncUrl, } from './lib/athena-urls';
|
|
207
|
+
export { athenaLinkAppUrl, createAthenaSpacesUrl, DEFAULT_APP_URL, DEFAULT_STATEWIRE_SYNC_URL, deriveAthenaApiUrl, deriveAthenaAppUrl, deriveStatewireSyncUrl, resolveStatewireSyncUrl, } from './lib/athena-urls';
|
|
208
208
|
export type { AthenaEnvironment } from './lib/athena-urls';
|
|
209
209
|
export { Button, buttonVariants } from './ui/button';
|
|
210
210
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './ui/tooltip';
|
package/dist/index.js
CHANGED
|
@@ -66,7 +66,7 @@ import { useAssetPanelStore } from "./assets/asset-panel-store.js";
|
|
|
66
66
|
import { SESSION_WORKBENCH_ASSETS_PANE_ID, SessionWorkbench } from "./workbench/SessionWorkbench.js";
|
|
67
67
|
import { SESSION_WORKSPACE_LIVE_REFETCH_MS, useSessionWorkbench, useSessionWorkbenchWorkspace } from "./workbench/session-workbench-context.js";
|
|
68
68
|
import { DEFAULT_SESSION_WORKBENCH_OPEN_PANE_IDS, DEFAULT_SESSION_WORKBENCH_PANES, SESSION_WORKBENCH_ATTACHMENTS_PANE, SESSION_WORKBENCH_COMPUTER_PANE, SESSION_WORKBENCH_PULL_REQUESTS_PANE, SESSION_WORKBENCH_SHELL_PANE, SESSION_WORKBENCH_TASKS_PANE, SessionAttachmentsPane, SessionComputerPane, SessionPullRequestsPane, SessionShellPane, SessionTasksPane } from "./workbench/session-workbench-panes.js";
|
|
69
|
-
import { SESSION_SANDBOX_LABEL, SHELL_OUTPUT_PREVIEW_CHARS, assetPanelTypeOf, assetTypeLabel,
|
|
69
|
+
import { SESSION_SANDBOX_LABEL, SHELL_OUTPUT_PREVIEW_CHARS, assetPanelTypeOf, assetTypeLabel, computerStateBadge, deriveShellRows, deriveTaskSteps, filterShellRows, filterWorkbenchPanes, reviewDecisionLabel, shellMachine, taskProgress, toolResultText, workspaceComputers } from "./workbench/session-workbench-model.js";
|
|
70
70
|
import { computerSshAccessPath, computerStatusPath, fetchComputerRuntimeStatus, fetchComputerSshAccess, fetchSessionWorkspace, readComputerRuntimeStatus, readComputerSshAccess, readSessionWorkspace, sessionWorkspacePath } from "./workbench/session-workspace-api.js";
|
|
71
71
|
import { useAthenaComputerSshAccess, useAthenaComputerStatus, useAthenaSessionWorkspace } from "./workbench/use-session-workspace.js";
|
|
72
72
|
import { ComputerLiveDesktop, LIVE_DESKTOP_READY_TIMEOUT_MS } from "./workbench/ComputerLiveDesktop.js";
|
|
@@ -112,7 +112,7 @@ import { ComposerQuotePreview } from "./composer/ComposerQuotePreview.js";
|
|
|
112
112
|
import { QuoteProvider, createQuoteStore, useQuote } from "./composer/QuoteContext.js";
|
|
113
113
|
import { AttachmentProvider, createAttachmentStore, useAttachments } from "./composer/AttachmentContext.js";
|
|
114
114
|
import { useParentAuth, useParentBridge } from "./runtime/auth.js";
|
|
115
|
-
import { DEFAULT_APP_URL, DEFAULT_STATEWIRE_SYNC_URL, createAthenaSpacesUrl, deriveAthenaApiUrl, deriveAthenaAppUrl, deriveStatewireSyncUrl, resolveStatewireSyncUrl } from "./lib/athena-urls.js";
|
|
115
|
+
import { DEFAULT_APP_URL, DEFAULT_STATEWIRE_SYNC_URL, athenaLinkAppUrl, createAthenaSpacesUrl, deriveAthenaApiUrl, deriveAthenaAppUrl, deriveStatewireSyncUrl, resolveStatewireSyncUrl } from "./lib/athena-urls.js";
|
|
116
116
|
import { Button, buttonVariants } from "./ui/button.js";
|
|
117
117
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip.js";
|
|
118
118
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible.js";
|
package/dist/lib/athena-urls.cjs
CHANGED
|
@@ -181,10 +181,27 @@ function createAthenaSpacesUrl({
|
|
|
181
181
|
}
|
|
182
182
|
return url.toString();
|
|
183
183
|
}
|
|
184
|
+
function originOf(url) {
|
|
185
|
+
try {
|
|
186
|
+
return new URL(url).origin;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function athenaLinkAppUrl({
|
|
192
|
+
appUrl,
|
|
193
|
+
backendUrl,
|
|
194
|
+
pageOrigin
|
|
195
|
+
}) {
|
|
196
|
+
const deployed = deriveAthenaAppUrl({ backendUrl });
|
|
197
|
+
if (deployed && pageOrigin !== null && originOf(appUrl) === pageOrigin) return deployed;
|
|
198
|
+
return appUrl;
|
|
199
|
+
}
|
|
184
200
|
exports.DEFAULT_APP_URL = DEFAULT_APP_URL;
|
|
185
201
|
exports.DEFAULT_ATHENA_ENVIRONMENT = DEFAULT_ATHENA_ENVIRONMENT;
|
|
186
202
|
exports.DEFAULT_STATEWIRE_SYNC_URL = DEFAULT_STATEWIRE_SYNC_URL;
|
|
187
203
|
exports.assertHostMintedStatewireMount = assertHostMintedStatewireMount;
|
|
204
|
+
exports.athenaLinkAppUrl = athenaLinkAppUrl;
|
|
188
205
|
exports.createAthenaSpacesUrl = createAthenaSpacesUrl;
|
|
189
206
|
exports.deriveAthenaApiUrl = deriveAthenaApiUrl;
|
|
190
207
|
exports.deriveAthenaAppUrl = deriveAthenaAppUrl;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"athena-urls.cjs","sources":["../../src/lib/athena-urls.ts"],"sourcesContent":["export type AthenaEnvironment = 'production' | 'staging';\n\nexport interface AthenaEnvironmentUrls {\n apiUrl: string;\n backendUrl: string;\n appUrl: string;\n}\n\n/**\n * Bare origins: `apiUrl` is the Iris host fronting the deployment (its origin\n * derives the `/v2/threads` statewire mount) and `backendUrl` is the Agora\n * REST base. Neither carries a path — `deriveStatewireSyncUrl` and the REST\n * helpers append their own.\n */\nconst ATHENA_ENVIRONMENT_URLS: Record<AthenaEnvironment, AthenaEnvironmentUrls> = {\n production: {\n apiUrl: 'https://iris.prd.athenaintel.com',\n backendUrl: 'https://api.athenaintel.com',\n appUrl: 'https://app.athenaintel.com',\n },\n staging: {\n apiUrl: 'https://iris.stg.athenaintel.com',\n backendUrl: 'https://staging-api.athenaintel.com',\n appUrl: 'https://staging-app.athenaintel.com',\n },\n};\n\nexport const DEFAULT_ATHENA_ENVIRONMENT: AthenaEnvironment = 'production';\nexport const DEFAULT_APP_URL = ATHENA_ENVIRONMENT_URLS.production.appUrl;\n/** Production statewire sync endpoint — the same host the mobile app and\n * Chrome extension ship as their production statewire base. Exported as a\n * constant for explicit configuration; it is deliberately NOT used as an\n * implicit fallback (see {@link resolveStatewireSyncUrl}). */\nexport const DEFAULT_STATEWIRE_SYNC_URL = 'https://iris.prd.athenaintel.com/v2/threads';\n\nconst STATEWIRE_SYNC_PATHNAME = '/v2/threads';\n\n/**\n * Derive the statewire sync endpoint (the `…/v2/threads` mount) from the Iris\n * host's origin: staging `https://iris.stg.athenaintel.com` maps to\n * `https://iris.stg.athenaintel.com/v2/threads`, and likewise for production\n * and workspace Iris hosts (a URL still carrying the retired `/api/chat` path\n * resolves the same way). Unparseable inputs return `null` so callers can\n * fall back explicitly.\n */\nexport function deriveStatewireSyncUrl({\n apiUrl,\n}: {\n apiUrl?: string | null;\n}): string | null {\n if (!apiUrl) return null;\n\n try {\n const parsedUrl = new URL(apiUrl);\n return `${parsedUrl.origin}${STATEWIRE_SYNC_PATHNAME}`;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the statewire sync endpoint for a provider configuration: an\n * explicit `statewireSyncUrl` wins, otherwise it is derived from the\n * effective `apiUrl` host.\n *\n * When neither yields a URL this throws instead of falling back to an Athena\n * default — mirroring `deriveAthenaApiUrl`'s null-for-unknown-hosts rule, so\n * a misconfigured custom deployment can never silently send its bearer token\n * or API key to Athena production.\n */\nexport function resolveStatewireSyncUrl({\n statewireSyncUrl,\n apiUrl,\n}: {\n statewireSyncUrl?: string;\n apiUrl?: string | null;\n}): string {\n if (statewireSyncUrl) return statewireSyncUrl;\n\n const derived = deriveStatewireSyncUrl({ apiUrl });\n if (derived) return derived;\n\n throw new Error(\n `[AthenaSDK] transport 'statewire' could not derive a sync URL from apiUrl (${JSON.stringify(\n apiUrl ?? null,\n )}). Pass an explicit statewireSyncUrl (the …/v2/threads mount). Credentials are never sent to a default host.`,\n );\n}\n\n/**\n * Refuse to attach a host-authenticated app to a guessed host. A host that\n * answered the auth bridge (a `/_athena/auth` mint, an `athena-config`\n * message) but minted no statewire mount predates the statewire-native\n * mint; without an explicit `statewireSyncUrl` or `apiUrl` the provider\n * would otherwise fall back to the environment preset and send that host's\n * credential to managed-cloud Athena. No host answering at all is not this\n * case: the preset is the documented default for a standalone app.\n */\nexport function assertHostMintedStatewireMount({\n hostAnswered,\n hostStatewireSyncUrl,\n statewireSyncUrl,\n apiUrl,\n}: {\n hostAnswered: boolean;\n hostStatewireSyncUrl: string | null;\n statewireSyncUrl?: string;\n apiUrl?: string;\n}): void {\n if (!hostAnswered || hostStatewireSyncUrl || statewireSyncUrl || apiUrl) return;\n throw new Error(\n '[AthenaSDK] The host answered the auth bridge without a statewire sync mount (statewireSyncUrl) the SDK accepted: either the host predates the statewire-native mint (update it: Marathon >= 2026-08-29), or the parent page sent a URL the SDK refused (see the auth.bridge.config_rejected diagnostics event: it must be https, on an Athena host or under the parent page\\'s own domain). Or configure statewireSyncUrl / apiUrl explicitly. Credentials are never sent to a default host.',\n );\n}\n\nconst MARATHON_APP_PORT = '8082';\nconst SPACES_PATHNAME = 'dashboard/spaces/';\n\nconst normalizeBaseUrl = (url: string): string => url.replace(/\\/+$/, '');\n\nconst createUrlWithFallback = (url: string, fallbackUrl: string): URL => {\n try {\n return new URL(`${normalizeBaseUrl(url)}/`);\n } catch {\n return new URL(`${normalizeBaseUrl(fallbackUrl)}/`);\n }\n};\n\nconst getHostname = (value?: string | null): string | null => {\n if (!value) return null;\n\n try {\n return new URL(value).hostname;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceAppUrl = ({\n sourceUrl,\n sourcePort,\n}: {\n sourceUrl: string;\n sourcePort: '8000' | '8787';\n}): string | null => {\n try {\n const parsedUrl = new URL(sourceUrl);\n const match = parsedUrl.hostname.match(\n new RegExp(`^(?<prefix>.+)--${sourcePort}\\\\.(?<domain>app\\\\.athenaintel\\\\.com|staging-app\\\\.athenaintel\\\\.com)$`),\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--${MARATHON_APP_PORT}.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceApiUrl = (backendUrl: string): string | null => {\n try {\n const parsedUrl = new URL(backendUrl);\n const match = parsedUrl.hostname.match(\n /^(?<prefix>.+)--8000\\.(?<domain>app\\.athenaintel\\.com|staging-app\\.athenaintel\\.com)$/,\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--8787.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nexport function getAthenaEnvironmentUrls({\n environment = DEFAULT_ATHENA_ENVIRONMENT,\n}: {\n environment?: AthenaEnvironment;\n} = {}): AthenaEnvironmentUrls {\n return ATHENA_ENVIRONMENT_URLS[environment];\n}\n\n/**\n * Resolve the Iris origin that fronts a known Agora backend.\n *\n * Unknown custom backends deliberately return `null`: callers can preserve a\n * direct-Agora fallback instead of accidentally sending a custom environment's\n * bearer token and traffic through production Iris.\n */\nexport function deriveAthenaApiUrl({\n backendUrl,\n}: {\n backendUrl?: string | null;\n}): string | null {\n if (!backendUrl) return null;\n\n const workspaceApiUrl = deriveWorkspaceApiUrl(backendUrl);\n if (workspaceApiUrl) return workspaceApiUrl;\n\n try {\n const parsedUrl = new URL(backendUrl);\n if (parsedUrl.hostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.apiUrl;\n }\n if (parsedUrl.hostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.apiUrl;\n }\n if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {\n parsedUrl.port = '8787';\n return parsedUrl.origin;\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nexport function deriveAthenaAppUrl({\n apiUrl,\n backendUrl,\n}: {\n apiUrl?: string | null;\n backendUrl?: string | null;\n}): string | null {\n if (backendUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: backendUrl,\n sourcePort: '8000',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const backendHostname = getHostname(backendUrl);\n if (backendHostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (backendHostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n if (apiUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: apiUrl,\n sourcePort: '8787',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const apiHostname = getHostname(apiUrl);\n if (apiHostname === 'iris.prd.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (apiHostname === 'iris.stg.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n return null;\n}\n\nexport function createAthenaSpacesUrl({\n appUrl = DEFAULT_APP_URL,\n assetIds,\n sessionId,\n}: {\n appUrl?: string;\n assetIds?: string | string[];\n sessionId?: string;\n}): string {\n const url = new URL(SPACES_PATHNAME, createUrlWithFallback(appUrl, DEFAULT_APP_URL));\n\n if (assetIds) {\n const normalizedAssetIds = (Array.isArray(assetIds) ? assetIds : [assetIds])\n .map((assetId) => assetId.trim())\n .filter((assetId) => assetId.length > 0);\n\n if (normalizedAssetIds.length > 0) {\n url.searchParams.set('asset_ids', normalizedAssetIds.join(','));\n }\n }\n\n if (sessionId) {\n url.searchParams.set('session_id', sessionId);\n }\n\n return url.toString();\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"athena-urls.cjs","sources":["../../src/lib/athena-urls.ts"],"sourcesContent":["export type AthenaEnvironment = 'production' | 'staging';\n\nexport interface AthenaEnvironmentUrls {\n apiUrl: string;\n backendUrl: string;\n appUrl: string;\n}\n\n/**\n * Bare origins: `apiUrl` is the Iris host fronting the deployment (its origin\n * derives the `/v2/threads` statewire mount) and `backendUrl` is the Agora\n * REST base. Neither carries a path — `deriveStatewireSyncUrl` and the REST\n * helpers append their own.\n */\nconst ATHENA_ENVIRONMENT_URLS: Record<AthenaEnvironment, AthenaEnvironmentUrls> = {\n production: {\n apiUrl: 'https://iris.prd.athenaintel.com',\n backendUrl: 'https://api.athenaintel.com',\n appUrl: 'https://app.athenaintel.com',\n },\n staging: {\n apiUrl: 'https://iris.stg.athenaintel.com',\n backendUrl: 'https://staging-api.athenaintel.com',\n appUrl: 'https://staging-app.athenaintel.com',\n },\n};\n\nexport const DEFAULT_ATHENA_ENVIRONMENT: AthenaEnvironment = 'production';\nexport const DEFAULT_APP_URL = ATHENA_ENVIRONMENT_URLS.production.appUrl;\n/** Production statewire sync endpoint — the same host the mobile app and\n * Chrome extension ship as their production statewire base. Exported as a\n * constant for explicit configuration; it is deliberately NOT used as an\n * implicit fallback (see {@link resolveStatewireSyncUrl}). */\nexport const DEFAULT_STATEWIRE_SYNC_URL = 'https://iris.prd.athenaintel.com/v2/threads';\n\nconst STATEWIRE_SYNC_PATHNAME = '/v2/threads';\n\n/**\n * Derive the statewire sync endpoint (the `…/v2/threads` mount) from the Iris\n * host's origin: staging `https://iris.stg.athenaintel.com` maps to\n * `https://iris.stg.athenaintel.com/v2/threads`, and likewise for production\n * and workspace Iris hosts (a URL still carrying the retired `/api/chat` path\n * resolves the same way). Unparseable inputs return `null` so callers can\n * fall back explicitly.\n */\nexport function deriveStatewireSyncUrl({\n apiUrl,\n}: {\n apiUrl?: string | null;\n}): string | null {\n if (!apiUrl) return null;\n\n try {\n const parsedUrl = new URL(apiUrl);\n return `${parsedUrl.origin}${STATEWIRE_SYNC_PATHNAME}`;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the statewire sync endpoint for a provider configuration: an\n * explicit `statewireSyncUrl` wins, otherwise it is derived from the\n * effective `apiUrl` host.\n *\n * When neither yields a URL this throws instead of falling back to an Athena\n * default — mirroring `deriveAthenaApiUrl`'s null-for-unknown-hosts rule, so\n * a misconfigured custom deployment can never silently send its bearer token\n * or API key to Athena production.\n */\nexport function resolveStatewireSyncUrl({\n statewireSyncUrl,\n apiUrl,\n}: {\n statewireSyncUrl?: string;\n apiUrl?: string | null;\n}): string {\n if (statewireSyncUrl) return statewireSyncUrl;\n\n const derived = deriveStatewireSyncUrl({ apiUrl });\n if (derived) return derived;\n\n throw new Error(\n `[AthenaSDK] transport 'statewire' could not derive a sync URL from apiUrl (${JSON.stringify(\n apiUrl ?? null,\n )}). Pass an explicit statewireSyncUrl (the …/v2/threads mount). Credentials are never sent to a default host.`,\n );\n}\n\n/**\n * Refuse to attach a host-authenticated app to a guessed host. A host that\n * answered the auth bridge (a `/_athena/auth` mint, an `athena-config`\n * message) but minted no statewire mount predates the statewire-native\n * mint; without an explicit `statewireSyncUrl` or `apiUrl` the provider\n * would otherwise fall back to the environment preset and send that host's\n * credential to managed-cloud Athena. No host answering at all is not this\n * case: the preset is the documented default for a standalone app.\n */\nexport function assertHostMintedStatewireMount({\n hostAnswered,\n hostStatewireSyncUrl,\n statewireSyncUrl,\n apiUrl,\n}: {\n hostAnswered: boolean;\n hostStatewireSyncUrl: string | null;\n statewireSyncUrl?: string;\n apiUrl?: string;\n}): void {\n if (!hostAnswered || hostStatewireSyncUrl || statewireSyncUrl || apiUrl) return;\n throw new Error(\n '[AthenaSDK] The host answered the auth bridge without a statewire sync mount (statewireSyncUrl) the SDK accepted: either the host predates the statewire-native mint (update it: Marathon >= 2026-08-29), or the parent page sent a URL the SDK refused (see the auth.bridge.config_rejected diagnostics event: it must be https, on an Athena host or under the parent page\\'s own domain). Or configure statewireSyncUrl / apiUrl explicitly. Credentials are never sent to a default host.',\n );\n}\n\nconst MARATHON_APP_PORT = '8082';\nconst SPACES_PATHNAME = 'dashboard/spaces/';\n\nconst normalizeBaseUrl = (url: string): string => url.replace(/\\/+$/, '');\n\nconst createUrlWithFallback = (url: string, fallbackUrl: string): URL => {\n try {\n return new URL(`${normalizeBaseUrl(url)}/`);\n } catch {\n return new URL(`${normalizeBaseUrl(fallbackUrl)}/`);\n }\n};\n\nconst getHostname = (value?: string | null): string | null => {\n if (!value) return null;\n\n try {\n return new URL(value).hostname;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceAppUrl = ({\n sourceUrl,\n sourcePort,\n}: {\n sourceUrl: string;\n sourcePort: '8000' | '8787';\n}): string | null => {\n try {\n const parsedUrl = new URL(sourceUrl);\n const match = parsedUrl.hostname.match(\n new RegExp(`^(?<prefix>.+)--${sourcePort}\\\\.(?<domain>app\\\\.athenaintel\\\\.com|staging-app\\\\.athenaintel\\\\.com)$`),\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--${MARATHON_APP_PORT}.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nconst deriveWorkspaceApiUrl = (backendUrl: string): string | null => {\n try {\n const parsedUrl = new URL(backendUrl);\n const match = parsedUrl.hostname.match(\n /^(?<prefix>.+)--8000\\.(?<domain>app\\.athenaintel\\.com|staging-app\\.athenaintel\\.com)$/,\n );\n\n if (!match?.groups?.prefix || !match.groups.domain) {\n return null;\n }\n\n return `${parsedUrl.protocol}//${match.groups.prefix}--8787.${match.groups.domain}`;\n } catch {\n return null;\n }\n};\n\nexport function getAthenaEnvironmentUrls({\n environment = DEFAULT_ATHENA_ENVIRONMENT,\n}: {\n environment?: AthenaEnvironment;\n} = {}): AthenaEnvironmentUrls {\n return ATHENA_ENVIRONMENT_URLS[environment];\n}\n\n/**\n * Resolve the Iris origin that fronts a known Agora backend.\n *\n * Unknown custom backends deliberately return `null`: callers can preserve a\n * direct-Agora fallback instead of accidentally sending a custom environment's\n * bearer token and traffic through production Iris.\n */\nexport function deriveAthenaApiUrl({\n backendUrl,\n}: {\n backendUrl?: string | null;\n}): string | null {\n if (!backendUrl) return null;\n\n const workspaceApiUrl = deriveWorkspaceApiUrl(backendUrl);\n if (workspaceApiUrl) return workspaceApiUrl;\n\n try {\n const parsedUrl = new URL(backendUrl);\n if (parsedUrl.hostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.apiUrl;\n }\n if (parsedUrl.hostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.apiUrl;\n }\n if (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1') {\n parsedUrl.port = '8787';\n return parsedUrl.origin;\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nexport function deriveAthenaAppUrl({\n apiUrl,\n backendUrl,\n}: {\n apiUrl?: string | null;\n backendUrl?: string | null;\n}): string | null {\n if (backendUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: backendUrl,\n sourcePort: '8000',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const backendHostname = getHostname(backendUrl);\n if (backendHostname === 'api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (backendHostname === 'staging-api.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n if (apiUrl) {\n const workspaceAppUrl = deriveWorkspaceAppUrl({\n sourceUrl: apiUrl,\n sourcePort: '8787',\n });\n if (workspaceAppUrl) return workspaceAppUrl;\n\n const apiHostname = getHostname(apiUrl);\n if (apiHostname === 'iris.prd.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.production.appUrl;\n }\n if (apiHostname === 'iris.stg.athenaintel.com') {\n return ATHENA_ENVIRONMENT_URLS.staging.appUrl;\n }\n }\n\n return null;\n}\n\nexport function createAthenaSpacesUrl({\n appUrl = DEFAULT_APP_URL,\n assetIds,\n sessionId,\n}: {\n appUrl?: string;\n assetIds?: string | string[];\n sessionId?: string;\n}): string {\n const url = new URL(SPACES_PATHNAME, createUrlWithFallback(appUrl, DEFAULT_APP_URL));\n\n if (assetIds) {\n const normalizedAssetIds = (Array.isArray(assetIds) ? assetIds : [assetIds])\n .map((assetId) => assetId.trim())\n .filter((assetId) => assetId.length > 0);\n\n if (normalizedAssetIds.length > 0) {\n url.searchParams.set('asset_ids', normalizedAssetIds.join(','));\n }\n }\n\n if (sessionId) {\n url.searchParams.set('session_id', sessionId);\n }\n\n return url.toString();\n}\n\nfunction originOf(url: string): string | null {\n try {\n return new URL(url).origin;\n } catch {\n return null;\n }\n}\n\n/**\n * The Athena frontend an \"Open in Athena\" link points at. The provider's\n * `appUrl` is that frontend, except in an app a computer serves: Marathon's\n * `/_athena/auth` mints `appUrl` as the app's OWN origin, so a link built on\n * it opens the app instead of Athena. When `appUrl` is the page's own origin\n * and the Agora base belongs to a known Athena deployment, the deployment's\n * frontend wins (on Olympus itself the two are the same origin anyway). The\n * workbench's \"Open in Athena\" links and the chat's asset chips both go by it.\n */\nexport function athenaLinkAppUrl({\n appUrl,\n backendUrl,\n pageOrigin,\n}: {\n appUrl: string;\n backendUrl: string;\n pageOrigin: string | null;\n}): string {\n const deployed = deriveAthenaAppUrl({ backendUrl });\n if (deployed && pageOrigin !== null && originOf(appUrl) === pageOrigin) return deployed;\n return appUrl;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -73,3 +73,17 @@ export declare function createAthenaSpacesUrl({ appUrl, assetIds, sessionId, }:
|
|
|
73
73
|
assetIds?: string | string[];
|
|
74
74
|
sessionId?: string;
|
|
75
75
|
}): string;
|
|
76
|
+
/**
|
|
77
|
+
* The Athena frontend an "Open in Athena" link points at. The provider's
|
|
78
|
+
* `appUrl` is that frontend, except in an app a computer serves: Marathon's
|
|
79
|
+
* `/_athena/auth` mints `appUrl` as the app's OWN origin, so a link built on
|
|
80
|
+
* it opens the app instead of Athena. When `appUrl` is the page's own origin
|
|
81
|
+
* and the Agora base belongs to a known Athena deployment, the deployment's
|
|
82
|
+
* frontend wins (on Olympus itself the two are the same origin anyway). The
|
|
83
|
+
* workbench's "Open in Athena" links and the chat's asset chips both go by it.
|
|
84
|
+
*/
|
|
85
|
+
export declare function athenaLinkAppUrl({ appUrl, backendUrl, pageOrigin, }: {
|
|
86
|
+
appUrl: string;
|
|
87
|
+
backendUrl: string;
|
|
88
|
+
pageOrigin: string | null;
|
|
89
|
+
}): string;
|
package/dist/lib/athena-urls.js
CHANGED
|
@@ -179,11 +179,28 @@ function createAthenaSpacesUrl({
|
|
|
179
179
|
}
|
|
180
180
|
return url.toString();
|
|
181
181
|
}
|
|
182
|
+
function originOf(url) {
|
|
183
|
+
try {
|
|
184
|
+
return new URL(url).origin;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function athenaLinkAppUrl({
|
|
190
|
+
appUrl,
|
|
191
|
+
backendUrl,
|
|
192
|
+
pageOrigin
|
|
193
|
+
}) {
|
|
194
|
+
const deployed = deriveAthenaAppUrl({ backendUrl });
|
|
195
|
+
if (deployed && pageOrigin !== null && originOf(appUrl) === pageOrigin) return deployed;
|
|
196
|
+
return appUrl;
|
|
197
|
+
}
|
|
182
198
|
export {
|
|
183
199
|
DEFAULT_APP_URL,
|
|
184
200
|
DEFAULT_ATHENA_ENVIRONMENT,
|
|
185
201
|
DEFAULT_STATEWIRE_SYNC_URL,
|
|
186
202
|
assertHostMintedStatewireMount,
|
|
203
|
+
athenaLinkAppUrl,
|
|
187
204
|
createAthenaSpacesUrl,
|
|
188
205
|
deriveAthenaApiUrl,
|
|
189
206
|
deriveAthenaAppUrl,
|