@someonegg/agent-hub 0.0.0-stage → 0.7.0
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/LICENSE +21 -0
- package/README.md +104 -2
- package/README.zh-CN.md +99 -0
- package/README.zh-TW.md +99 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/index-4ejyszaV.css +1 -0
- package/dist/client/assets/index-CKKT4cMK.js +309 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/favicon.svg +6 -0
- package/dist/client/icons/icon-192.png +0 -0
- package/dist/client/icons/icon-512.png +0 -0
- package/dist/client/icons/icon-maskable-512.png +0 -0
- package/dist/client/index.html +22 -0
- package/dist/client/manifest.webmanifest +21 -0
- package/dist/server/server/adapters/claude/claude-session-source.js +220 -0
- package/dist/server/server/adapters/claude/identity.js +28 -0
- package/dist/server/server/adapters/claude/interaction.js +68 -0
- package/dist/server/server/adapters/claude/prompt-readiness.js +12 -0
- package/dist/server/server/adapters/claude/ready-script.js +25 -0
- package/dist/server/server/adapters/claude/session-manager.js +106 -0
- package/dist/server/server/adapters/claude/session-normalizer.js +173 -0
- package/dist/server/server/adapters/claude/user-input-normalizer.js +71 -0
- package/dist/server/server/adapters/codex/catalog-source.js +2 -0
- package/dist/server/server/adapters/codex/codex-session-source.js +250 -0
- package/dist/server/server/adapters/codex/identity-resolver.js +104 -0
- package/dist/server/server/adapters/codex/interaction-parser.js +70 -0
- package/dist/server/server/adapters/codex/io-errors.js +2 -0
- package/dist/server/server/adapters/codex/jsonl-catalog-source.js +84 -0
- package/dist/server/server/adapters/codex/limits.js +2 -0
- package/dist/server/server/adapters/codex/message-normalizer.js +66 -0
- package/dist/server/server/adapters/codex/response-item-parser.js +64 -0
- package/dist/server/server/adapters/codex/rollout-descriptor.js +2 -0
- package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
- package/dist/server/server/adapters/codex/session-manager.js +51 -0
- package/dist/server/server/adapters/codex/session-normalizer.js +175 -0
- package/dist/server/server/adapters/codex/subagent-identity.js +9 -0
- package/dist/server/server/adapters/codex/user-input-normalizer.js +121 -0
- package/dist/server/server/adapters/codex/value-readers.js +4 -0
- package/dist/server/server/adapters/shared/binding-protocol.js +54 -0
- package/dist/server/server/adapters/shared/io-errors.js +13 -0
- package/dist/server/server/adapters/shared/jsonl-decoder.js +263 -0
- package/dist/server/server/adapters/shared/limits.js +4 -0
- package/dist/server/server/adapters/shared/session-management.js +43 -0
- package/dist/server/server/adapters/shared/source-cache.js +23 -0
- package/dist/server/server/api/session-api-mapper.js +65 -0
- package/dist/server/server/application/interaction-session-reader.js +17 -0
- package/dist/server/server/application/session-read-service.js +86 -0
- package/dist/server/server/application/session-reader.js +2 -0
- package/dist/server/server/application/session-response.js +11 -0
- package/dist/server/server/catalog/config.js +4 -0
- package/dist/server/server/catalog/session-catalog-service.js +112 -0
- package/dist/server/server/cli/command-error.js +27 -0
- package/dist/server/server/cli/main.js +42 -0
- package/dist/server/server/cli/output.js +52 -0
- package/dist/server/server/cli/resource.js +104 -0
- package/dist/server/server/cli/session-client.js +64 -0
- package/dist/server/server/cli/session-follow.js +58 -0
- package/dist/server/server/cli/session-output.js +184 -0
- package/dist/server/server/cli/session-wait.js +44 -0
- package/dist/server/server/cli/session.js +318 -0
- package/dist/server/server/cli/skill.js +37 -0
- package/dist/server/server/config.js +196 -0
- package/dist/server/server/create-hub.js +91 -0
- package/dist/server/server/directory-lock.js +65 -0
- package/dist/server/server/domain/project-directory.js +5 -0
- package/dist/server/server/domain/session-domain.js +2 -0
- package/dist/server/server/domain/session-text.js +45 -0
- package/dist/server/server/hosting/diff-store.js +103 -0
- package/dist/server/server/hosting/routes.js +168 -0
- package/dist/server/server/hosting/store.js +162 -0
- package/dist/server/server/hosting/validation-error.js +4 -0
- package/dist/server/server/http/api-route-handler.js +2 -0
- package/dist/server/server/http/api-router.js +54 -0
- package/dist/server/server/http/create-server.js +218 -0
- package/dist/server/server/http/query-params.js +19 -0
- package/dist/server/server/http/request-body.js +48 -0
- package/dist/server/server/http/request-errors.js +59 -0
- package/dist/server/server/http/router.js +16 -0
- package/dist/server/server/http/security.js +19 -0
- package/dist/server/server/http/session-interaction-routes.js +128 -0
- package/dist/server/server/http/session-management-routes.js +105 -0
- package/dist/server/server/http/session-path.js +10 -0
- package/dist/server/server/http/session-read-routes.js +143 -0
- package/dist/server/server/http/terminal-preview-websocket.js +179 -0
- package/dist/server/server/http/upload-routes.js +124 -0
- package/dist/server/server/interaction/interaction-service.js +276 -0
- package/dist/server/server/interaction/message.js +19 -0
- package/dist/server/server/interaction/pane-binding-manager.js +62 -0
- package/dist/server/server/interaction/pane-observation.js +31 -0
- package/dist/server/server/interaction/reconcile-session-state.js +21 -0
- package/dist/server/server/interaction/runtime-capabilities.js +2 -0
- package/dist/server/server/interaction/session-runtime.js +86 -0
- package/dist/server/server/interaction/terminal-activity-service.js +81 -0
- package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
- package/dist/server/server/interaction/terminal-preview-service.js +184 -0
- package/dist/server/server/interaction/tmux-launcher.js +84 -0
- package/dist/server/server/interaction/tmux-preview-listener.js +107 -0
- package/dist/server/server/interaction/tmux-service.js +334 -0
- package/dist/server/server/interaction/tmux-target.js +13 -0
- package/dist/server/server/interaction/transport.js +9 -0
- package/dist/server/server/main.js +54 -0
- package/dist/server/server/management/operation-service.js +307 -0
- package/dist/server/server/management/operation-state.js +3 -0
- package/dist/server/server/management/session-exit-service.js +50 -0
- package/dist/server/server/management/session-launch-executor.js +164 -0
- package/dist/server/server/management/session-lifecycle-executor.js +158 -0
- package/dist/server/server/management/session-manager.js +64 -0
- package/dist/server/server/repository/catalog-snapshot-store.js +318 -0
- package/dist/server/server/repository/diagnostic-logger.js +70 -0
- package/dist/server/server/repository/refresh-coordinator.js +14 -0
- package/dist/server/server/repository/session-queries.js +72 -0
- package/dist/server/server/security/opaque-id.js +17 -0
- package/dist/server/server/source/session-source.js +2 -0
- package/dist/server/server/updates/session-updates-service.js +151 -0
- package/dist/server/server/updates/update-revision.js +22 -0
- package/dist/server/server/uploads/store.js +304 -0
- package/dist/server/server/version.js +21 -0
- package/dist/server/shared/api-contract.js +2 -0
- package/dist/server/shared/diff.js +158 -0
- package/dist/server/shared/domain.js +2 -0
- package/dist/server/shared/interaction.js +22 -0
- package/dist/server/shared/task-name.js +6 -0
- package/dist/server/shared/uploads.js +8 -0
- package/package.json +98 -4
- package/skills/ahub-control/SKILL.md +93 -0
- package/skills/ahub-publish/SKILL.md +54 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { validProjectDirectory } from "../domain/project-directory.js";
|
|
2
|
+
/** Time before startup needs terminal access, including adapter prompt readiness. */
|
|
3
|
+
export const LAUNCH_ATTACH_DELAY_MS = 30_000;
|
|
4
|
+
/** A launch failure records whether a process may already be running. */
|
|
5
|
+
export class SessionLaunchError extends Error {
|
|
6
|
+
outcome;
|
|
7
|
+
attachCommand;
|
|
8
|
+
target;
|
|
9
|
+
constructor(outcome, message, attachCommand = null, target) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.outcome = outcome;
|
|
12
|
+
this.attachCommand = attachCommand;
|
|
13
|
+
this.target = target;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class SessionManagerRegistry {
|
|
17
|
+
defaultAgent;
|
|
18
|
+
#managers = new Map();
|
|
19
|
+
constructor(defaultAgent) {
|
|
20
|
+
this.defaultAgent = defaultAgent;
|
|
21
|
+
}
|
|
22
|
+
register(agent, manager) {
|
|
23
|
+
if (!agent.trim())
|
|
24
|
+
throw new Error("Empty agent nickname");
|
|
25
|
+
if (this.#managers.has(agent))
|
|
26
|
+
throw new Error(`Duplicate agent nickname: ${agent}`);
|
|
27
|
+
this.#managers.set(agent, manager);
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
resolveCreateAgent(agent) {
|
|
31
|
+
const selected = agent ?? this.defaultAgent;
|
|
32
|
+
if (!this.#managers.has(selected))
|
|
33
|
+
throw new Error("Unknown agent nickname");
|
|
34
|
+
return selected;
|
|
35
|
+
}
|
|
36
|
+
get(agent) {
|
|
37
|
+
const manager = this.#managers.get(agent);
|
|
38
|
+
if (!manager) {
|
|
39
|
+
throw new Error(`Session source has no management capability: ${agent}`);
|
|
40
|
+
}
|
|
41
|
+
return manager;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function resolveResumeDirectory(manager, session) {
|
|
45
|
+
for (const candidate of new Set([session.workingDirectory, session.projectDirectory])) {
|
|
46
|
+
if (!validProjectDirectory(candidate))
|
|
47
|
+
continue;
|
|
48
|
+
try {
|
|
49
|
+
return await manager.resolveDirectory(candidate);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const code = error.code;
|
|
53
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new Error("Session has no available working or project directory");
|
|
58
|
+
}
|
|
59
|
+
export function managementCapabilities(manager) {
|
|
60
|
+
return {
|
|
61
|
+
archive: typeof manager.archive === "function",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=session-manager.js.map
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { encodeStringTuple, opaqueIdForParts } from "../security/opaque-id.js";
|
|
2
|
+
import { RefreshDiagnosticLogger, } from "./diagnostic-logger.js";
|
|
3
|
+
import { RefreshCoordinator } from "./refresh-coordinator.js";
|
|
4
|
+
export const DEFAULT_CATALOG_FRESHNESS_MS = 1_500;
|
|
5
|
+
export const MAX_CATALOG_DIAGNOSTICS = 50;
|
|
6
|
+
export class CatalogSnapshotStore {
|
|
7
|
+
freshnessMs;
|
|
8
|
+
now;
|
|
9
|
+
#coordinator = new RefreshCoordinator();
|
|
10
|
+
#diagnosticLogger;
|
|
11
|
+
#sources;
|
|
12
|
+
#snapshot = null;
|
|
13
|
+
#observedSources = new Map();
|
|
14
|
+
#pendingBindings = new Map();
|
|
15
|
+
#publishBindings;
|
|
16
|
+
#aggregate = null;
|
|
17
|
+
#aggregateDiagnostics = new Map();
|
|
18
|
+
#lastDiscoveryAt = Number.NEGATIVE_INFINITY;
|
|
19
|
+
constructor(sources, freshnessMs = DEFAULT_CATALOG_FRESHNESS_MS, now = performance.now.bind(performance), dependencies = {}) {
|
|
20
|
+
this.freshnessMs = freshnessMs;
|
|
21
|
+
this.now = now;
|
|
22
|
+
assertUniqueSources(sources);
|
|
23
|
+
this.#publishBindings = dependencies.publishBindings ?? (() => { });
|
|
24
|
+
this.#diagnosticLogger = new RefreshDiagnosticLogger(dependencies.diagnosticLogger);
|
|
25
|
+
this.#sources = [...sources];
|
|
26
|
+
}
|
|
27
|
+
async current() {
|
|
28
|
+
const snapshot = this.#snapshot;
|
|
29
|
+
if (snapshot !== null && this.now() - this.#lastDiscoveryAt < this.freshnessMs) {
|
|
30
|
+
return snapshot;
|
|
31
|
+
}
|
|
32
|
+
return this.#coordinator.run(() => this.#discover());
|
|
33
|
+
}
|
|
34
|
+
async refresh() {
|
|
35
|
+
return this.#coordinator.run(() => this.#discover());
|
|
36
|
+
}
|
|
37
|
+
async #discover() {
|
|
38
|
+
const snapshot = await this.#rebuild();
|
|
39
|
+
const bindings = [...this.#pendingBindings].flatMap(([source, events]) => [...events].map(({ localId, ...event }) => ({
|
|
40
|
+
...event,
|
|
41
|
+
sessionId: opaqueIdForParts(source.descriptor.instanceKey, localId),
|
|
42
|
+
})));
|
|
43
|
+
this.#publishBindings(bindings);
|
|
44
|
+
this.#pendingBindings.clear();
|
|
45
|
+
this.#lastDiscoveryAt = this.now();
|
|
46
|
+
return snapshot;
|
|
47
|
+
}
|
|
48
|
+
async #rebuild() {
|
|
49
|
+
// Wait for every source even on failure, so retries cannot overlap an unfinished refresh.
|
|
50
|
+
const results = await Promise.allSettled(this.#sources.map(async (source) => {
|
|
51
|
+
const snapshot = await source.refresh();
|
|
52
|
+
if (this.#observedSources.get(source) !== snapshot) {
|
|
53
|
+
const pending = this.#pendingBindings.get(source) ?? new Set();
|
|
54
|
+
for (const event of snapshot.bindings ?? [])
|
|
55
|
+
pending.add(event);
|
|
56
|
+
this.#pendingBindings.set(source, pending);
|
|
57
|
+
this.#observedSources.set(source, snapshot);
|
|
58
|
+
}
|
|
59
|
+
return { descriptor: source.descriptor, snapshot };
|
|
60
|
+
}));
|
|
61
|
+
const loadedSources = results.map((result) => {
|
|
62
|
+
if (result.status === "rejected")
|
|
63
|
+
throw result.reason;
|
|
64
|
+
return result.value;
|
|
65
|
+
});
|
|
66
|
+
const signature = aggregateSignature(loadedSources);
|
|
67
|
+
const previous = this.#snapshot;
|
|
68
|
+
if (previous?.signature === signature) {
|
|
69
|
+
this.#diagnosticLogger.update(loadedSources, this.#aggregateDiagnostics);
|
|
70
|
+
return previous;
|
|
71
|
+
}
|
|
72
|
+
const sourceDiagnostics = [];
|
|
73
|
+
for (const { snapshot } of loadedSources) {
|
|
74
|
+
if (sourceDiagnostics.length >= MAX_CATALOG_DIAGNOSTICS)
|
|
75
|
+
break;
|
|
76
|
+
for (const item of snapshot.diagnostics) {
|
|
77
|
+
if (sourceDiagnostics.length >= MAX_CATALOG_DIAGNOSTICS)
|
|
78
|
+
break;
|
|
79
|
+
sourceDiagnostics.push({ ...item });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const linked = linkRelationships(loadedSources, this.#aggregate, MAX_CATALOG_DIAGNOSTICS - sourceDiagnostics.length);
|
|
83
|
+
const diagnostics = [...sourceDiagnostics, ...linked.diagnostics];
|
|
84
|
+
const normalizedSessions = linked.sessions;
|
|
85
|
+
const snapshot = {
|
|
86
|
+
signature,
|
|
87
|
+
diagnostics,
|
|
88
|
+
sessions: normalizedSessions,
|
|
89
|
+
};
|
|
90
|
+
this.#aggregate = {
|
|
91
|
+
inputs: linked.inputs,
|
|
92
|
+
nativeBuckets: linked.nativeBuckets,
|
|
93
|
+
parentNativeDependents: linked.parentNativeDependents,
|
|
94
|
+
resolvedParents: linked.resolvedParents,
|
|
95
|
+
sessions: normalizedSessions,
|
|
96
|
+
};
|
|
97
|
+
this.#snapshot = snapshot;
|
|
98
|
+
this.#aggregateDiagnostics = linked.aggregateDiagnostics;
|
|
99
|
+
this.#diagnosticLogger.update(loadedSources, linked.aggregateDiagnostics);
|
|
100
|
+
return snapshot;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function assertUniqueSources(sources) {
|
|
104
|
+
for (const field of ["instanceKey", "agent"]) {
|
|
105
|
+
const keys = new Set();
|
|
106
|
+
for (const { descriptor } of sources) {
|
|
107
|
+
const key = descriptor[field];
|
|
108
|
+
if (!key.trim())
|
|
109
|
+
throw new Error(`Empty session source ${field}`);
|
|
110
|
+
if (keys.has(key)) {
|
|
111
|
+
const label = field === "instanceKey" ? "instance key" : field;
|
|
112
|
+
throw new Error(`Duplicate session source ${label}: ${key}`);
|
|
113
|
+
}
|
|
114
|
+
keys.add(key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function aggregateSignature(sources) {
|
|
119
|
+
return JSON.stringify([...sources]
|
|
120
|
+
.sort((left, right) => left.descriptor.instanceKey.localeCompare(right.descriptor.instanceKey))
|
|
121
|
+
.map(({ descriptor, snapshot }) => ({
|
|
122
|
+
source: descriptor.instanceKey,
|
|
123
|
+
signature: snapshot.signature,
|
|
124
|
+
})));
|
|
125
|
+
}
|
|
126
|
+
function linkRelationships(sources, previous, diagnosticLimit) {
|
|
127
|
+
const diagnostics = [];
|
|
128
|
+
const aggregateDiagnostics = new Map();
|
|
129
|
+
const inputs = new Map();
|
|
130
|
+
const nativeBuckets = new Map();
|
|
131
|
+
const parentNativeDependents = new Map();
|
|
132
|
+
for (const { descriptor, snapshot } of sources) {
|
|
133
|
+
const localIds = new Set();
|
|
134
|
+
for (const entry of snapshot.sessions) {
|
|
135
|
+
if (localIds.has(entry.localId)) {
|
|
136
|
+
const diagnostic = {
|
|
137
|
+
code: "duplicate_source_session_id",
|
|
138
|
+
severity: "error",
|
|
139
|
+
message: "A source returned duplicate stable session identities.",
|
|
140
|
+
ordinal: null,
|
|
141
|
+
};
|
|
142
|
+
if (diagnostics.length < diagnosticLimit)
|
|
143
|
+
diagnostics.push(diagnostic);
|
|
144
|
+
const group = aggregateDiagnostics.get(descriptor.agent) ?? [];
|
|
145
|
+
const sessionId = opaqueIdForParts(descriptor.instanceKey, entry.localId);
|
|
146
|
+
if (group.length < MAX_CATALOG_DIAGNOSTICS &&
|
|
147
|
+
!group.some((item) => item.sessionId === sessionId)) {
|
|
148
|
+
group.push({ sessionId, diagnostic });
|
|
149
|
+
aggregateDiagnostics.set(descriptor.agent, group);
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
localIds.add(entry.localId);
|
|
154
|
+
const id = opaqueIdForParts(descriptor.instanceKey, entry.localId);
|
|
155
|
+
const input = {
|
|
156
|
+
descriptor,
|
|
157
|
+
id,
|
|
158
|
+
nativeSessionId: entry.nativeSessionId,
|
|
159
|
+
parentNativeSessionId: entry.parentNativeSessionId,
|
|
160
|
+
origin: entry.origin,
|
|
161
|
+
normalized: entry.normalized,
|
|
162
|
+
};
|
|
163
|
+
inputs.set(id, input);
|
|
164
|
+
if (entry.nativeSessionId !== null) {
|
|
165
|
+
const nativeKey = identityKey(descriptor.instanceKey, entry.nativeSessionId);
|
|
166
|
+
const bucket = nativeBuckets.get(nativeKey);
|
|
167
|
+
if (bucket === undefined)
|
|
168
|
+
nativeBuckets.set(nativeKey, [id]);
|
|
169
|
+
else
|
|
170
|
+
bucket.push(id);
|
|
171
|
+
}
|
|
172
|
+
if (entry.parentNativeSessionId !== null) {
|
|
173
|
+
const parentKey = identityKey(descriptor.instanceKey, entry.parentNativeSessionId);
|
|
174
|
+
const dependents = parentNativeDependents.get(parentKey);
|
|
175
|
+
if (dependents === undefined) {
|
|
176
|
+
parentNativeDependents.set(parentKey, new Set([id]));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
dependents.add(id);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const resolvedParents = new Map();
|
|
185
|
+
const children = new Map();
|
|
186
|
+
for (const item of inputs.values()) {
|
|
187
|
+
const parentId = item.parentNativeSessionId === null
|
|
188
|
+
? null
|
|
189
|
+
: uniqueIdentity(nativeBuckets.get(identityKey(item.descriptor.instanceKey, item.parentNativeSessionId)));
|
|
190
|
+
resolvedParents.set(item.id, parentId);
|
|
191
|
+
if (parentId !== null) {
|
|
192
|
+
const childIds = children.get(parentId);
|
|
193
|
+
if (childIds === undefined)
|
|
194
|
+
children.set(parentId, [item.id]);
|
|
195
|
+
else
|
|
196
|
+
childIds.push(item.id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const dirtyIds = findDirtyIds(inputs, nativeBuckets, parentNativeDependents, resolvedParents, previous);
|
|
200
|
+
const sessions = new Map();
|
|
201
|
+
for (const [id, item] of inputs) {
|
|
202
|
+
const cached = previous?.sessions.get(id);
|
|
203
|
+
if (cached !== undefined && !dirtyIds.has(id)) {
|
|
204
|
+
sessions.set(id, cached);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const source = item.normalized.session;
|
|
208
|
+
sessions.set(id, {
|
|
209
|
+
session: {
|
|
210
|
+
...source,
|
|
211
|
+
id,
|
|
212
|
+
sourceId: item.nativeSessionId,
|
|
213
|
+
origin: {
|
|
214
|
+
...item.origin,
|
|
215
|
+
sourceType: item.descriptor.sourceType,
|
|
216
|
+
agent: item.descriptor.agent,
|
|
217
|
+
runtimeName: item.descriptor.displayName,
|
|
218
|
+
},
|
|
219
|
+
isChild: item.parentNativeSessionId !== null,
|
|
220
|
+
parentId: resolvedParents.get(id) ?? null,
|
|
221
|
+
childIds: [...(children.get(id) ?? [])].sort(compareCodeUnits),
|
|
222
|
+
},
|
|
223
|
+
timeline: item.normalized.timeline,
|
|
224
|
+
interaction: item.normalized.interaction ?? null,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
sessions,
|
|
229
|
+
diagnostics,
|
|
230
|
+
aggregateDiagnostics,
|
|
231
|
+
dirtyIds,
|
|
232
|
+
inputs,
|
|
233
|
+
nativeBuckets,
|
|
234
|
+
parentNativeDependents,
|
|
235
|
+
resolvedParents,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function findDirtyIds(inputs, nativeBuckets, parentNativeDependents, resolvedParents, previous) {
|
|
239
|
+
const dirty = new Set();
|
|
240
|
+
if (previous === null) {
|
|
241
|
+
for (const id of inputs.keys())
|
|
242
|
+
dirty.add(id);
|
|
243
|
+
return dirty;
|
|
244
|
+
}
|
|
245
|
+
for (const [id, input] of inputs) {
|
|
246
|
+
const oldInput = previous.inputs.get(id);
|
|
247
|
+
// SourceSessionEntry wrappers may be rebuilt on every refresh. The Codex
|
|
248
|
+
// adapter reuses unchanged immutable NormalizedSession objects, so identity
|
|
249
|
+
// is only a safe optimization hint: a new object means "recompute", never
|
|
250
|
+
// that the public revision must change. The final-view digest decides that.
|
|
251
|
+
if (oldInput === undefined || !sameSourceInput(oldInput, input)) {
|
|
252
|
+
dirty.add(id);
|
|
253
|
+
}
|
|
254
|
+
const oldParent = previous.resolvedParents.get(id) ?? null;
|
|
255
|
+
const newParent = resolvedParents.get(id) ?? null;
|
|
256
|
+
if (oldInput === undefined || oldParent !== newParent) {
|
|
257
|
+
dirty.add(id);
|
|
258
|
+
if (oldParent !== null && inputs.has(oldParent))
|
|
259
|
+
dirty.add(oldParent);
|
|
260
|
+
if (newParent !== null)
|
|
261
|
+
dirty.add(newParent);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const [id] of previous.inputs) {
|
|
265
|
+
if (inputs.has(id))
|
|
266
|
+
continue;
|
|
267
|
+
const oldParent = previous.resolvedParents.get(id) ?? null;
|
|
268
|
+
if (oldParent !== null && inputs.has(oldParent))
|
|
269
|
+
dirty.add(oldParent);
|
|
270
|
+
}
|
|
271
|
+
const identityKeys = new Set([...previous.nativeBuckets.keys(), ...nativeBuckets.keys()]);
|
|
272
|
+
for (const key of identityKeys) {
|
|
273
|
+
if (uniqueIdentity(previous.nativeBuckets.get(key)) === uniqueIdentity(nativeBuckets.get(key))) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const dependents = new Set([
|
|
277
|
+
...(previous.parentNativeDependents.get(key) ?? []),
|
|
278
|
+
...(parentNativeDependents.get(key) ?? []),
|
|
279
|
+
]);
|
|
280
|
+
for (const id of dependents) {
|
|
281
|
+
if (!inputs.has(id))
|
|
282
|
+
continue;
|
|
283
|
+
dirty.add(id);
|
|
284
|
+
const oldParent = previous.resolvedParents.get(id) ?? null;
|
|
285
|
+
const newParent = resolvedParents.get(id) ?? null;
|
|
286
|
+
if (oldParent !== null && inputs.has(oldParent))
|
|
287
|
+
dirty.add(oldParent);
|
|
288
|
+
if (newParent !== null)
|
|
289
|
+
dirty.add(newParent);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return dirty;
|
|
293
|
+
}
|
|
294
|
+
function sameSourceInput(left, right) {
|
|
295
|
+
return (left.normalized === right.normalized &&
|
|
296
|
+
left.nativeSessionId === right.nativeSessionId &&
|
|
297
|
+
left.parentNativeSessionId === right.parentNativeSessionId &&
|
|
298
|
+
sameOrigin(left.origin, right.origin) &&
|
|
299
|
+
left.descriptor.sourceType === right.descriptor.sourceType &&
|
|
300
|
+
left.descriptor.agent === right.descriptor.agent &&
|
|
301
|
+
left.descriptor.displayName === right.descriptor.displayName);
|
|
302
|
+
}
|
|
303
|
+
function sameOrigin(left, right) {
|
|
304
|
+
return (left.sourceType === right.sourceType &&
|
|
305
|
+
left.agent === right.agent &&
|
|
306
|
+
left.runtimeName === right.runtimeName &&
|
|
307
|
+
left.runtimeVersion === right.runtimeVersion);
|
|
308
|
+
}
|
|
309
|
+
function uniqueIdentity(bucket) {
|
|
310
|
+
return bucket?.length === 1 ? bucket[0] : null;
|
|
311
|
+
}
|
|
312
|
+
function identityKey(sourceKey, nativeId) {
|
|
313
|
+
return encodeStringTuple(sourceKey, nativeId);
|
|
314
|
+
}
|
|
315
|
+
function compareCodeUnits(left, right) {
|
|
316
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=catalog-snapshot-store.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { opaqueIdForParts } from "../security/opaque-id.js";
|
|
2
|
+
const stderrLogger = {
|
|
3
|
+
log(record) {
|
|
4
|
+
console.error("Session diagnostic", record);
|
|
5
|
+
},
|
|
6
|
+
};
|
|
7
|
+
/** Refresh-only state: bounded by the current sources and sessions. */
|
|
8
|
+
export class RefreshDiagnosticLogger {
|
|
9
|
+
logger;
|
|
10
|
+
#sources = new Map();
|
|
11
|
+
constructor(logger = stderrLogger) {
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
update(sources, aggregates = new Map()) {
|
|
15
|
+
const next = new Map();
|
|
16
|
+
for (const { descriptor, snapshot } of sources) {
|
|
17
|
+
const agent = descriptor.agent;
|
|
18
|
+
const previous = this.#sources.get(agent);
|
|
19
|
+
if (previous?.snapshot === snapshot) {
|
|
20
|
+
next.set(agent, previous);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const diagnostics = new Map();
|
|
24
|
+
const record = (key, items, scope, sessionId) => {
|
|
25
|
+
const prior = previous?.diagnostics.get(key);
|
|
26
|
+
if (prior?.reference === items) {
|
|
27
|
+
diagnostics.set(key, prior);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const signatures = new Set();
|
|
31
|
+
for (const item of items.slice(0, 50)) {
|
|
32
|
+
const signature = JSON.stringify([item.severity, item.code, item.message, item.ordinal]);
|
|
33
|
+
if (!signatures.has(signature) && !prior?.signatures.has(signature)) {
|
|
34
|
+
try {
|
|
35
|
+
this.logger.log({
|
|
36
|
+
agent,
|
|
37
|
+
sessionId,
|
|
38
|
+
scope,
|
|
39
|
+
severity: item.severity,
|
|
40
|
+
code: item.code,
|
|
41
|
+
message: item.message,
|
|
42
|
+
ordinal: item.ordinal,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* Logging must never affect reading or management. */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
signatures.add(signature);
|
|
50
|
+
}
|
|
51
|
+
diagnostics.set(key, { reference: items, signatures });
|
|
52
|
+
};
|
|
53
|
+
record("source", snapshot.diagnostics, "source", null);
|
|
54
|
+
const ids = new Set();
|
|
55
|
+
for (const entry of snapshot.sessions) {
|
|
56
|
+
const sessionId = opaqueIdForParts(descriptor.instanceKey, entry.localId);
|
|
57
|
+
if (ids.has(entry.localId))
|
|
58
|
+
continue;
|
|
59
|
+
record(`session:${sessionId}`, entry.normalized.session.diagnostics, "session", sessionId);
|
|
60
|
+
ids.add(entry.localId);
|
|
61
|
+
}
|
|
62
|
+
for (const { sessionId, diagnostic } of aggregates.get(agent) ?? []) {
|
|
63
|
+
record(`aggregate:${sessionId}`, [diagnostic], "aggregate", sessionId);
|
|
64
|
+
}
|
|
65
|
+
next.set(agent, { snapshot, diagnostics });
|
|
66
|
+
}
|
|
67
|
+
this.#sources = next;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=diagnostic-logger.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class RefreshCoordinator {
|
|
2
|
+
#inFlight = null;
|
|
3
|
+
run(refresh) {
|
|
4
|
+
if (this.#inFlight !== null)
|
|
5
|
+
return this.#inFlight;
|
|
6
|
+
const pending = refresh().finally(() => {
|
|
7
|
+
if (this.#inFlight === pending)
|
|
8
|
+
this.#inFlight = null;
|
|
9
|
+
});
|
|
10
|
+
this.#inFlight = pending;
|
|
11
|
+
return pending;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=refresh-coordinator.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const MAX_ITEM_PAGE_BYTES = 4 * 1024 * 1024;
|
|
2
|
+
export class RepositoryQueryError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class SessionQueries {
|
|
10
|
+
// Published timeline records are immutable; release cached sizes with their records.
|
|
11
|
+
#itemBytes = new WeakMap();
|
|
12
|
+
session(snapshot, id) {
|
|
13
|
+
return snapshot.sessions.get(id)?.session ?? null;
|
|
14
|
+
}
|
|
15
|
+
messages(snapshot, id, query) {
|
|
16
|
+
validateItemQuery(query);
|
|
17
|
+
if (query.after !== undefined && (!Number.isSafeInteger(query.after) || query.after < 0))
|
|
18
|
+
throw new RepositoryQueryError("invalid_query", "after must be a nonnegative safe integer");
|
|
19
|
+
const normalized = snapshot.sessions.get(id);
|
|
20
|
+
if (!normalized)
|
|
21
|
+
return null;
|
|
22
|
+
const timeline = normalized.timeline;
|
|
23
|
+
const end = timeline.at(-1)?.ordinal ?? 0;
|
|
24
|
+
if ((query.after ?? 0) > end)
|
|
25
|
+
throw new RepositoryQueryError("after_out_of_range", "after exceeds the current timeline range");
|
|
26
|
+
const limit = query.limit ?? 10;
|
|
27
|
+
let start = 0;
|
|
28
|
+
if (query.after === undefined) {
|
|
29
|
+
start = Math.max(0, timeline.length - limit);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
let upper = timeline.length;
|
|
33
|
+
while (start < upper) {
|
|
34
|
+
const middle = start + Math.floor((upper - start) / 2);
|
|
35
|
+
if (timeline[middle].ordinal <= query.after)
|
|
36
|
+
start = middle + 1;
|
|
37
|
+
else
|
|
38
|
+
upper = middle;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const items = [];
|
|
42
|
+
let bytes = 0;
|
|
43
|
+
let index = start;
|
|
44
|
+
for (; index < timeline.length; index++) {
|
|
45
|
+
const item = timeline[index];
|
|
46
|
+
if (items.length >= limit)
|
|
47
|
+
break;
|
|
48
|
+
let itemBytes = this.#itemBytes.get(item);
|
|
49
|
+
if (itemBytes === undefined) {
|
|
50
|
+
itemBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
|
|
51
|
+
this.#itemBytes.set(item, itemBytes);
|
|
52
|
+
}
|
|
53
|
+
const size = itemBytes + (items.length ? 1 : 0);
|
|
54
|
+
if (items.length > 0 && bytes + size > MAX_ITEM_PAGE_BYTES)
|
|
55
|
+
break;
|
|
56
|
+
items.push(item);
|
|
57
|
+
bytes += size;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
session: normalized.session,
|
|
61
|
+
items,
|
|
62
|
+
nextAfter: items.at(-1)?.ordinal ?? end,
|
|
63
|
+
hasMore: index < timeline.length,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function validateItemQuery(query) {
|
|
68
|
+
if (query.limit !== undefined &&
|
|
69
|
+
(!Number.isInteger(query.limit) || query.limit < 1 || query.limit > 300))
|
|
70
|
+
throw new RepositoryQueryError("invalid_query", "limit must be between 1 and 300");
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=session-queries.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export function encodeStringTuple(...parts) {
|
|
3
|
+
return parts.map((part) => `${part.length}:${part}`).join("");
|
|
4
|
+
}
|
|
5
|
+
export function opaqueIdForParts(...parts) {
|
|
6
|
+
return createHash("sha256")
|
|
7
|
+
.update(encodeStringTuple(...parts), "utf16le")
|
|
8
|
+
.digest("base64url");
|
|
9
|
+
}
|
|
10
|
+
export function opaqueIdForPath(path) {
|
|
11
|
+
return createHash("sha256").update(path, "utf8").digest("base64url");
|
|
12
|
+
}
|
|
13
|
+
export const SESSION_ID_PATTERN = "[A-Za-z0-9_-]{20,100}";
|
|
14
|
+
export function isOpaqueId(value) {
|
|
15
|
+
return new RegExp(`^${SESSION_ID_PATTERN}$`).test(value);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=opaque-id.js.map
|