@someonegg/codex-hub 0.0.0-stage → 0.1.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 +101 -2
- 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-Dx1LBBmn.js +304 -0
- package/dist/client/assets/index-lmwuA3r2.css +1 -0
- package/dist/client/index.html +16 -0
- package/dist/server/server/adapters/codex/agent-identity.js +9 -0
- package/dist/server/server/adapters/codex/catalog-source.js +2 -0
- package/dist/server/server/adapters/codex/codex-session-source.js +300 -0
- package/dist/server/server/adapters/codex/identity-resolver.js +107 -0
- package/dist/server/server/adapters/codex/interaction-parser.js +90 -0
- package/dist/server/server/adapters/codex/internal-event-parser.js +56 -0
- package/dist/server/server/adapters/codex/io-errors.js +5 -0
- package/dist/server/server/adapters/codex/jsonl-catalog-source.js +58 -0
- package/dist/server/server/adapters/codex/limits.js +10 -0
- package/dist/server/server/adapters/codex/message-normalizer.js +121 -0
- package/dist/server/server/adapters/codex/path-policy.js +88 -0
- package/dist/server/server/adapters/codex/project-directory-resolver.js +79 -0
- package/dist/server/server/adapters/codex/response-item-parser.js +100 -0
- package/dist/server/server/adapters/codex/rollout-decoder.js +286 -0
- package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
- package/dist/server/server/adapters/codex/session-manager.js +62 -0
- package/dist/server/server/adapters/codex/session-normalizer.js +257 -0
- package/dist/server/server/adapters/codex/tool-normalizer.js +58 -0
- package/dist/server/server/adapters/codex/user-input-normalizer.js +123 -0
- package/dist/server/server/api/session-api-mapper.js +194 -0
- package/dist/server/server/application/session-read-service.js +109 -0
- package/dist/server/server/application/session-reader.js +2 -0
- package/dist/server/server/catalog/config.js +4 -0
- package/dist/server/server/catalog/session-catalog-service.js +103 -0
- package/dist/server/server/config.js +145 -0
- package/dist/server/server/create-hub.js +64 -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/http/api-route-handler.js +2 -0
- package/dist/server/server/http/api-router.js +63 -0
- package/dist/server/server/http/create-server.js +148 -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/router.js +16 -0
- package/dist/server/server/http/security.js +19 -0
- package/dist/server/server/http/session-interaction-routes.js +101 -0
- package/dist/server/server/http/session-management-routes.js +90 -0
- package/dist/server/server/http/session-path.js +13 -0
- package/dist/server/server/http/session-read-routes.js +198 -0
- package/dist/server/server/http/session-updates-routes.js +84 -0
- package/dist/server/server/http/terminal-preview-websocket.js +179 -0
- package/dist/server/server/http/upload-routes.js +74 -0
- package/dist/server/server/interaction/interaction-service.js +247 -0
- package/dist/server/server/interaction/message.js +19 -0
- package/dist/server/server/interaction/pane-binding-manager.js +60 -0
- package/dist/server/server/interaction/reconcile-session-state.js +21 -0
- package/dist/server/server/interaction/session-runtime.js +86 -0
- package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
- package/dist/server/server/interaction/terminal-preview-service.js +194 -0
- package/dist/server/server/interaction/tmux-launcher.js +75 -0
- package/dist/server/server/interaction/tmux-preview-listener.js +101 -0
- package/dist/server/server/interaction/tmux-service.js +269 -0
- package/dist/server/server/interaction/transport.js +9 -0
- package/dist/server/server/main.js +41 -0
- package/dist/server/server/management/operation-service.js +540 -0
- package/dist/server/server/management/session-exit-service.js +50 -0
- package/dist/server/server/management/session-manager.js +59 -0
- package/dist/server/server/repository/catalog-snapshot-store.js +304 -0
- package/dist/server/server/repository/diagnostic-logger.js +70 -0
- package/dist/server/server/repository/opaque-cursor.js +79 -0
- package/dist/server/server/repository/refresh-coordinator.js +14 -0
- package/dist/server/server/repository/session-queries.js +151 -0
- package/dist/server/server/repository/session-query-criteria.js +2 -0
- package/dist/server/server/repository/timeline-prefix-index.js +342 -0
- package/dist/server/server/repository/timeline-prefix-registry.js +55 -0
- package/dist/server/server/security/opaque-id.js +13 -0
- package/dist/server/server/source/session-source.js +2 -0
- package/dist/server/server/updates/session-updates-service.js +147 -0
- package/dist/server/server/updates/update-revision.js +25 -0
- package/dist/server/shared/api-contract.js +2 -0
- package/dist/server/shared/domain.js +2 -0
- package/dist/server/shared/interaction.js +12 -0
- package/dist/server/shared/uploads.js +2 -0
- package/package.json +79 -4
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { encodeStringTuple, opaqueIdForParts } from "../security/opaque-id.js";
|
|
2
|
+
import { RefreshDiagnosticLogger, } from "./diagnostic-logger.js";
|
|
3
|
+
import { RefreshCoordinator } from "./refresh-coordinator.js";
|
|
4
|
+
import { TimelinePrefixRegistry, } from "./timeline-prefix-registry.js";
|
|
5
|
+
export const DEFAULT_CATALOG_FRESHNESS_MS = 1_500;
|
|
6
|
+
export const MAX_CATALOG_DIAGNOSTICS = 50;
|
|
7
|
+
export class CatalogSnapshotStore {
|
|
8
|
+
freshnessMs;
|
|
9
|
+
now;
|
|
10
|
+
#coordinator = new RefreshCoordinator();
|
|
11
|
+
#diagnosticLogger;
|
|
12
|
+
#prefixes;
|
|
13
|
+
#sources;
|
|
14
|
+
#snapshot = null;
|
|
15
|
+
#aggregate = null;
|
|
16
|
+
#aggregateDiagnostics = new Map();
|
|
17
|
+
#lastDiscoveryAt = Number.NEGATIVE_INFINITY;
|
|
18
|
+
constructor(sources, freshnessMs = DEFAULT_CATALOG_FRESHNESS_MS, now = performance.now.bind(performance), dependencies = {}) {
|
|
19
|
+
this.freshnessMs = freshnessMs;
|
|
20
|
+
this.now = now;
|
|
21
|
+
assertUniqueSources(sources);
|
|
22
|
+
this.#diagnosticLogger = new RefreshDiagnosticLogger(dependencies.diagnosticLogger);
|
|
23
|
+
this.#sources = [...sources];
|
|
24
|
+
this.#prefixes = new TimelinePrefixRegistry(dependencies.timelinePrefixIndexBuilder);
|
|
25
|
+
}
|
|
26
|
+
async current() {
|
|
27
|
+
const snapshot = this.#snapshot;
|
|
28
|
+
if (snapshot !== null && this.now() - this.#lastDiscoveryAt < this.freshnessMs) {
|
|
29
|
+
return snapshot;
|
|
30
|
+
}
|
|
31
|
+
return this.#coordinator.run(() => this.#discover());
|
|
32
|
+
}
|
|
33
|
+
async refresh() {
|
|
34
|
+
return this.#coordinator.run(() => this.#discover());
|
|
35
|
+
}
|
|
36
|
+
async #discover() {
|
|
37
|
+
const snapshot = await this.#rebuild();
|
|
38
|
+
this.#lastDiscoveryAt = this.now();
|
|
39
|
+
return snapshot;
|
|
40
|
+
}
|
|
41
|
+
async #rebuild() {
|
|
42
|
+
const loadedSources = await Promise.all(this.#sources.map(async (source) => ({
|
|
43
|
+
descriptor: source.descriptor,
|
|
44
|
+
snapshot: await source.refresh(),
|
|
45
|
+
})));
|
|
46
|
+
const signature = aggregateSignature(loadedSources);
|
|
47
|
+
const previous = this.#snapshot;
|
|
48
|
+
if (previous?.signature === signature) {
|
|
49
|
+
this.#diagnosticLogger.update(loadedSources, this.#aggregateDiagnostics);
|
|
50
|
+
return previous;
|
|
51
|
+
}
|
|
52
|
+
const sourceDiagnostics = [];
|
|
53
|
+
for (const { snapshot } of loadedSources) {
|
|
54
|
+
if (sourceDiagnostics.length >= MAX_CATALOG_DIAGNOSTICS)
|
|
55
|
+
break;
|
|
56
|
+
for (const item of snapshot.diagnostics) {
|
|
57
|
+
if (sourceDiagnostics.length >= MAX_CATALOG_DIAGNOSTICS)
|
|
58
|
+
break;
|
|
59
|
+
sourceDiagnostics.push({ ...item });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const linked = linkRelationships(loadedSources, this.#aggregate, MAX_CATALOG_DIAGNOSTICS - sourceDiagnostics.length);
|
|
63
|
+
const diagnostics = [...sourceDiagnostics, ...linked.diagnostics];
|
|
64
|
+
const normalizedSessions = linked.sessions;
|
|
65
|
+
const preparedPrefixes = this.#prefixes.prepare(normalizedSessions, linked.dirtyIds);
|
|
66
|
+
const snapshot = {
|
|
67
|
+
signature,
|
|
68
|
+
diagnostics,
|
|
69
|
+
sessions: preparedPrefixes.sessions,
|
|
70
|
+
};
|
|
71
|
+
preparedPrefixes.commit();
|
|
72
|
+
this.#aggregate = {
|
|
73
|
+
inputs: linked.inputs,
|
|
74
|
+
nativeBuckets: linked.nativeBuckets,
|
|
75
|
+
parentNativeDependents: linked.parentNativeDependents,
|
|
76
|
+
resolvedParents: linked.resolvedParents,
|
|
77
|
+
sessions: normalizedSessions,
|
|
78
|
+
};
|
|
79
|
+
this.#snapshot = snapshot;
|
|
80
|
+
this.#aggregateDiagnostics = linked.aggregateDiagnostics;
|
|
81
|
+
this.#diagnosticLogger.update(loadedSources, linked.aggregateDiagnostics);
|
|
82
|
+
return snapshot;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function assertUniqueSources(sources) {
|
|
86
|
+
for (const field of ["instanceKey", "sourceInstanceId"]) {
|
|
87
|
+
const keys = new Set();
|
|
88
|
+
for (const { descriptor } of sources) {
|
|
89
|
+
const key = descriptor[field];
|
|
90
|
+
if (!key.trim())
|
|
91
|
+
throw new Error(`Empty session source ${field}`);
|
|
92
|
+
if (keys.has(key)) {
|
|
93
|
+
const label = field === "instanceKey" ? "instance key" : field;
|
|
94
|
+
throw new Error(`Duplicate session source ${label}: ${key}`);
|
|
95
|
+
}
|
|
96
|
+
keys.add(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function aggregateSignature(sources) {
|
|
101
|
+
return JSON.stringify([...sources]
|
|
102
|
+
.sort((left, right) => left.descriptor.instanceKey.localeCompare(right.descriptor.instanceKey))
|
|
103
|
+
.map(({ descriptor, snapshot }) => ({
|
|
104
|
+
source: descriptor.instanceKey,
|
|
105
|
+
signature: snapshot.signature,
|
|
106
|
+
})));
|
|
107
|
+
}
|
|
108
|
+
function linkRelationships(sources, previous, diagnosticLimit) {
|
|
109
|
+
const diagnostics = [];
|
|
110
|
+
const aggregateDiagnostics = new Map();
|
|
111
|
+
const inputs = new Map();
|
|
112
|
+
const nativeBuckets = new Map();
|
|
113
|
+
const parentNativeDependents = new Map();
|
|
114
|
+
for (const { descriptor, snapshot } of sources) {
|
|
115
|
+
const localIds = new Set();
|
|
116
|
+
for (const entry of snapshot.sessions) {
|
|
117
|
+
if (localIds.has(entry.localId)) {
|
|
118
|
+
const diagnostic = {
|
|
119
|
+
code: "duplicate_source_session_id",
|
|
120
|
+
severity: "error",
|
|
121
|
+
message: "A source returned duplicate stable session identities.",
|
|
122
|
+
ordinal: null,
|
|
123
|
+
};
|
|
124
|
+
if (diagnostics.length < diagnosticLimit)
|
|
125
|
+
diagnostics.push(diagnostic);
|
|
126
|
+
const group = aggregateDiagnostics.get(descriptor.sourceInstanceId) ?? [];
|
|
127
|
+
const sessionId = opaqueIdForParts(descriptor.instanceKey, entry.localId);
|
|
128
|
+
if (group.length < MAX_CATALOG_DIAGNOSTICS &&
|
|
129
|
+
!group.some((item) => item.sessionId === sessionId)) {
|
|
130
|
+
group.push({ sessionId, diagnostic });
|
|
131
|
+
aggregateDiagnostics.set(descriptor.sourceInstanceId, group);
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
localIds.add(entry.localId);
|
|
136
|
+
const id = opaqueIdForParts(descriptor.instanceKey, entry.localId);
|
|
137
|
+
const input = {
|
|
138
|
+
descriptor,
|
|
139
|
+
id,
|
|
140
|
+
nativeSessionId: entry.nativeSessionId,
|
|
141
|
+
parentNativeSessionId: entry.parentNativeSessionId,
|
|
142
|
+
origin: entry.origin,
|
|
143
|
+
normalized: entry.normalized,
|
|
144
|
+
};
|
|
145
|
+
inputs.set(id, input);
|
|
146
|
+
if (entry.nativeSessionId !== null) {
|
|
147
|
+
const nativeKey = identityKey(descriptor.instanceKey, entry.nativeSessionId);
|
|
148
|
+
const bucket = nativeBuckets.get(nativeKey);
|
|
149
|
+
if (bucket === undefined)
|
|
150
|
+
nativeBuckets.set(nativeKey, [id]);
|
|
151
|
+
else
|
|
152
|
+
bucket.push(id);
|
|
153
|
+
}
|
|
154
|
+
if (entry.parentNativeSessionId !== null) {
|
|
155
|
+
const parentKey = identityKey(descriptor.instanceKey, entry.parentNativeSessionId);
|
|
156
|
+
const dependents = parentNativeDependents.get(parentKey);
|
|
157
|
+
if (dependents === undefined) {
|
|
158
|
+
parentNativeDependents.set(parentKey, new Set([id]));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
dependents.add(id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const resolvedParents = new Map();
|
|
167
|
+
const children = new Map();
|
|
168
|
+
for (const item of inputs.values()) {
|
|
169
|
+
const parentId = item.parentNativeSessionId === null
|
|
170
|
+
? null
|
|
171
|
+
: uniqueIdentity(nativeBuckets.get(identityKey(item.descriptor.instanceKey, item.parentNativeSessionId)));
|
|
172
|
+
resolvedParents.set(item.id, parentId);
|
|
173
|
+
if (parentId !== null) {
|
|
174
|
+
const childIds = children.get(parentId);
|
|
175
|
+
if (childIds === undefined)
|
|
176
|
+
children.set(parentId, [item.id]);
|
|
177
|
+
else
|
|
178
|
+
childIds.push(item.id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const dirtyIds = findDirtyIds(inputs, nativeBuckets, parentNativeDependents, resolvedParents, previous);
|
|
182
|
+
const sessions = new Map();
|
|
183
|
+
for (const [id, item] of inputs) {
|
|
184
|
+
const cached = previous?.sessions.get(id);
|
|
185
|
+
if (cached !== undefined && !dirtyIds.has(id)) {
|
|
186
|
+
sessions.set(id, cached);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const source = item.normalized.session;
|
|
190
|
+
sessions.set(id, {
|
|
191
|
+
session: {
|
|
192
|
+
...source,
|
|
193
|
+
id,
|
|
194
|
+
sourceId: item.nativeSessionId,
|
|
195
|
+
origin: {
|
|
196
|
+
...item.origin,
|
|
197
|
+
sourceType: item.descriptor.sourceType,
|
|
198
|
+
sourceInstanceId: item.descriptor.sourceInstanceId,
|
|
199
|
+
agentName: item.descriptor.displayName,
|
|
200
|
+
},
|
|
201
|
+
isChild: item.parentNativeSessionId !== null,
|
|
202
|
+
parentId: resolvedParents.get(id) ?? null,
|
|
203
|
+
childIds: [...(children.get(id) ?? [])].sort(compareCodeUnits),
|
|
204
|
+
},
|
|
205
|
+
timeline: item.normalized.timeline,
|
|
206
|
+
toolDetails: item.normalized.toolDetails,
|
|
207
|
+
directiveDetails: item.normalized.directiveDetails,
|
|
208
|
+
internalDetails: item.normalized.internalDetails,
|
|
209
|
+
interaction: item.normalized.interaction ?? null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
sessions,
|
|
214
|
+
diagnostics,
|
|
215
|
+
aggregateDiagnostics,
|
|
216
|
+
dirtyIds,
|
|
217
|
+
inputs,
|
|
218
|
+
nativeBuckets,
|
|
219
|
+
parentNativeDependents,
|
|
220
|
+
resolvedParents,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function findDirtyIds(inputs, nativeBuckets, parentNativeDependents, resolvedParents, previous) {
|
|
224
|
+
const dirty = new Set();
|
|
225
|
+
if (previous === null) {
|
|
226
|
+
for (const id of inputs.keys())
|
|
227
|
+
dirty.add(id);
|
|
228
|
+
return dirty;
|
|
229
|
+
}
|
|
230
|
+
for (const [id, input] of inputs) {
|
|
231
|
+
const oldInput = previous.inputs.get(id);
|
|
232
|
+
// SourceSessionEntry wrappers may be rebuilt on every refresh. The Codex
|
|
233
|
+
// adapter reuses unchanged immutable NormalizedSession objects, so identity
|
|
234
|
+
// is only a safe optimization hint: a new object means "recompute", never
|
|
235
|
+
// that the public revision must change. The final-view digest decides that.
|
|
236
|
+
if (oldInput === undefined || !sameSourceInput(oldInput, input)) {
|
|
237
|
+
dirty.add(id);
|
|
238
|
+
}
|
|
239
|
+
const oldParent = previous.resolvedParents.get(id) ?? null;
|
|
240
|
+
const newParent = resolvedParents.get(id) ?? null;
|
|
241
|
+
if (oldInput === undefined || oldParent !== newParent) {
|
|
242
|
+
dirty.add(id);
|
|
243
|
+
if (oldParent !== null && inputs.has(oldParent))
|
|
244
|
+
dirty.add(oldParent);
|
|
245
|
+
if (newParent !== null)
|
|
246
|
+
dirty.add(newParent);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const [id] of previous.inputs) {
|
|
250
|
+
if (inputs.has(id))
|
|
251
|
+
continue;
|
|
252
|
+
const oldParent = previous.resolvedParents.get(id) ?? null;
|
|
253
|
+
if (oldParent !== null && inputs.has(oldParent))
|
|
254
|
+
dirty.add(oldParent);
|
|
255
|
+
}
|
|
256
|
+
const identityKeys = new Set([...previous.nativeBuckets.keys(), ...nativeBuckets.keys()]);
|
|
257
|
+
for (const key of identityKeys) {
|
|
258
|
+
if (uniqueIdentity(previous.nativeBuckets.get(key)) === uniqueIdentity(nativeBuckets.get(key))) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const dependents = new Set([
|
|
262
|
+
...(previous.parentNativeDependents.get(key) ?? []),
|
|
263
|
+
...(parentNativeDependents.get(key) ?? []),
|
|
264
|
+
]);
|
|
265
|
+
for (const id of dependents) {
|
|
266
|
+
if (!inputs.has(id))
|
|
267
|
+
continue;
|
|
268
|
+
dirty.add(id);
|
|
269
|
+
const oldParent = previous.resolvedParents.get(id) ?? null;
|
|
270
|
+
const newParent = resolvedParents.get(id) ?? null;
|
|
271
|
+
if (oldParent !== null && inputs.has(oldParent))
|
|
272
|
+
dirty.add(oldParent);
|
|
273
|
+
if (newParent !== null)
|
|
274
|
+
dirty.add(newParent);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return dirty;
|
|
278
|
+
}
|
|
279
|
+
function sameSourceInput(left, right) {
|
|
280
|
+
return (left.normalized === right.normalized &&
|
|
281
|
+
left.nativeSessionId === right.nativeSessionId &&
|
|
282
|
+
left.parentNativeSessionId === right.parentNativeSessionId &&
|
|
283
|
+
sameOrigin(left.origin, right.origin) &&
|
|
284
|
+
left.descriptor.sourceType === right.descriptor.sourceType &&
|
|
285
|
+
left.descriptor.sourceInstanceId === right.descriptor.sourceInstanceId &&
|
|
286
|
+
left.descriptor.displayName === right.descriptor.displayName);
|
|
287
|
+
}
|
|
288
|
+
function sameOrigin(left, right) {
|
|
289
|
+
return (left.sourceType === right.sourceType &&
|
|
290
|
+
left.sourceInstanceId === right.sourceInstanceId &&
|
|
291
|
+
left.agentName === right.agentName &&
|
|
292
|
+
left.agentVersion === right.agentVersion &&
|
|
293
|
+
left.formatVersion === right.formatVersion);
|
|
294
|
+
}
|
|
295
|
+
function uniqueIdentity(bucket) {
|
|
296
|
+
return bucket?.length === 1 ? bucket[0] : null;
|
|
297
|
+
}
|
|
298
|
+
function identityKey(sourceKey, nativeId) {
|
|
299
|
+
return encodeStringTuple(sourceKey, nativeId);
|
|
300
|
+
}
|
|
301
|
+
function compareCodeUnits(left, right) {
|
|
302
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
303
|
+
}
|
|
304
|
+
//# 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 sourceId = descriptor.sourceInstanceId;
|
|
18
|
+
const previous = this.#sources.get(sourceId);
|
|
19
|
+
if (previous?.snapshot === snapshot) {
|
|
20
|
+
next.set(sourceId, 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
|
+
sourceInstanceId: sourceId,
|
|
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(sourceId) ?? []) {
|
|
63
|
+
record(`aggregate:${sessionId}`, [diagnostic], "aggregate", sessionId);
|
|
64
|
+
}
|
|
65
|
+
next.set(sourceId, { snapshot, diagnostics });
|
|
66
|
+
}
|
|
67
|
+
this.#sources = next;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=diagnostic-logger.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const SIGNATURE_BYTES = 24;
|
|
3
|
+
const SIGNATURE_CHARS = 32;
|
|
4
|
+
const MAX_CURSOR_CHARS = 16_384;
|
|
5
|
+
export class OpaqueCursorCodec {
|
|
6
|
+
key;
|
|
7
|
+
constructor(key = randomBytes(32)) {
|
|
8
|
+
this.key = key;
|
|
9
|
+
}
|
|
10
|
+
encodeTimeline(sessionId, ordinal, prefix) {
|
|
11
|
+
return this.#encode({ v: 1, s: sessionId, o: ordinal, p: prefix });
|
|
12
|
+
}
|
|
13
|
+
decodeTimeline(cursor) {
|
|
14
|
+
return this.#decode(cursor, isTimelineCursorPayload);
|
|
15
|
+
}
|
|
16
|
+
#encode(value) {
|
|
17
|
+
const payload = Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
18
|
+
const signature = createHmac("sha256", this.key)
|
|
19
|
+
.update(payload, "utf8")
|
|
20
|
+
.digest()
|
|
21
|
+
.subarray(0, SIGNATURE_BYTES)
|
|
22
|
+
.toString("base64url");
|
|
23
|
+
return `${payload}.${signature}`;
|
|
24
|
+
}
|
|
25
|
+
#decode(cursor, validate) {
|
|
26
|
+
if (cursor.length === 0 || cursor.length > MAX_CURSOR_CHARS) {
|
|
27
|
+
return { kind: "malformed" };
|
|
28
|
+
}
|
|
29
|
+
const parts = cursor.split(".");
|
|
30
|
+
if (parts.length !== 2)
|
|
31
|
+
return { kind: "malformed" };
|
|
32
|
+
const [payload, encodedSignature] = parts;
|
|
33
|
+
if (!payload ||
|
|
34
|
+
!encodedSignature ||
|
|
35
|
+
!BASE64URL.test(payload) ||
|
|
36
|
+
!BASE64URL.test(encodedSignature) ||
|
|
37
|
+
encodedSignature.length !== SIGNATURE_CHARS) {
|
|
38
|
+
return { kind: "malformed" };
|
|
39
|
+
}
|
|
40
|
+
let value;
|
|
41
|
+
try {
|
|
42
|
+
value = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { kind: "malformed" };
|
|
46
|
+
}
|
|
47
|
+
if (!validate(value))
|
|
48
|
+
return { kind: "malformed" };
|
|
49
|
+
const signature = Buffer.from(encodedSignature, "base64url");
|
|
50
|
+
const expected = createHmac("sha256", this.key)
|
|
51
|
+
.update(payload, "utf8")
|
|
52
|
+
.digest()
|
|
53
|
+
.subarray(0, SIGNATURE_BYTES);
|
|
54
|
+
if (signature.byteLength !== expected.byteLength || !timingSafeEqual(signature, expected)) {
|
|
55
|
+
return { kind: "untrusted" };
|
|
56
|
+
}
|
|
57
|
+
return { kind: "valid", value };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
61
|
+
function isTimelineCursorPayload(value) {
|
|
62
|
+
return (isRecord(value) &&
|
|
63
|
+
hasOnlyKeys(value, ["v", "s", "o", "p"]) &&
|
|
64
|
+
value.v === 1 &&
|
|
65
|
+
typeof value.s === "string" &&
|
|
66
|
+
isOrdinal(value.o) &&
|
|
67
|
+
typeof value.p === "string");
|
|
68
|
+
}
|
|
69
|
+
function isRecord(value) {
|
|
70
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
function isOrdinal(value) {
|
|
73
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
74
|
+
}
|
|
75
|
+
function hasOnlyKeys(value, keys) {
|
|
76
|
+
const actual = Object.keys(value);
|
|
77
|
+
return actual.length === keys.length && keys.every((key) => key in value);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=opaque-cursor.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,151 @@
|
|
|
1
|
+
import { OpaqueCursorCodec } from "./opaque-cursor.js";
|
|
2
|
+
const DEFAULT_ITEM_LIMIT = 100;
|
|
3
|
+
const MAX_ITEM_LIMIT = 300;
|
|
4
|
+
export const MAX_ITEM_PAGE_BYTES = 4 * 1024 * 1024;
|
|
5
|
+
export class RepositoryQueryError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class SessionQueries {
|
|
13
|
+
cursors;
|
|
14
|
+
constructor(cursors = new OpaqueCursorCodec()) {
|
|
15
|
+
this.cursors = cursors;
|
|
16
|
+
}
|
|
17
|
+
session(snapshot, id) {
|
|
18
|
+
const versioned = snapshot.sessions.get(id);
|
|
19
|
+
return versioned?.normalized.session ?? null;
|
|
20
|
+
}
|
|
21
|
+
items(snapshot, id, query) {
|
|
22
|
+
validateItemQuery(query);
|
|
23
|
+
const versioned = snapshot.sessions.get(id);
|
|
24
|
+
if (versioned === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
const requestedBoundary = this.#resolveReadBoundary(id, versioned, query.cursor);
|
|
27
|
+
const { normalized } = versioned;
|
|
28
|
+
const after = requestedBoundary.throughOrdinal;
|
|
29
|
+
let start = 0;
|
|
30
|
+
let end = normalized.timeline.length;
|
|
31
|
+
while (start < end) {
|
|
32
|
+
const middle = start + Math.floor((end - start) / 2);
|
|
33
|
+
if (normalized.timeline[middle].ordinal <= after)
|
|
34
|
+
start = middle + 1;
|
|
35
|
+
else
|
|
36
|
+
end = middle;
|
|
37
|
+
}
|
|
38
|
+
const limit = query.limit ?? DEFAULT_ITEM_LIMIT;
|
|
39
|
+
const items = [];
|
|
40
|
+
let itemBytes = 0;
|
|
41
|
+
for (let index = start; index < normalized.timeline.length && items.length < limit; index++) {
|
|
42
|
+
const item = normalized.timeline[index];
|
|
43
|
+
const bytes = Buffer.byteLength(JSON.stringify(item), "utf8") + (items.length > 0 ? 1 : 0);
|
|
44
|
+
if (items.length > 0 && itemBytes + bytes > MAX_ITEM_PAGE_BYTES)
|
|
45
|
+
break;
|
|
46
|
+
items.push(item);
|
|
47
|
+
itemBytes += bytes;
|
|
48
|
+
}
|
|
49
|
+
const boundary = items.length === 0
|
|
50
|
+
? requestedBoundary
|
|
51
|
+
: versioned.timelinePrefixIndex.boundaryAt(normalized.timeline, items.at(-1).ordinal);
|
|
52
|
+
return {
|
|
53
|
+
context: this.#readContext(id, versioned, boundary),
|
|
54
|
+
items,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
updates(snapshot, id, cursor) {
|
|
58
|
+
const versioned = snapshot.sessions.get(id);
|
|
59
|
+
if (versioned === undefined)
|
|
60
|
+
return null;
|
|
61
|
+
const boundary = this.#resolveReadBoundary(id, versioned, cursor);
|
|
62
|
+
return this.#readContext(id, versioned, boundary);
|
|
63
|
+
}
|
|
64
|
+
toolDetail(snapshot, id, itemId, query) {
|
|
65
|
+
const versioned = snapshot.sessions.get(id);
|
|
66
|
+
if (versioned === undefined)
|
|
67
|
+
return null;
|
|
68
|
+
const boundary = this.#resolveReadBoundary(id, versioned, query.cursor);
|
|
69
|
+
const detail = itemDetail(versioned.normalized, itemId, "tool", versioned.normalized.toolDetails, boundary.throughOrdinal);
|
|
70
|
+
if (detail === null)
|
|
71
|
+
return null;
|
|
72
|
+
return { detail };
|
|
73
|
+
}
|
|
74
|
+
directiveDetail(snapshot, id, itemId, query) {
|
|
75
|
+
const versioned = snapshot.sessions.get(id);
|
|
76
|
+
if (versioned === undefined)
|
|
77
|
+
return null;
|
|
78
|
+
const boundary = this.#resolveReadBoundary(id, versioned, query.cursor);
|
|
79
|
+
const detail = itemDetail(versioned.normalized, itemId, "directive", versioned.normalized.directiveDetails, boundary.throughOrdinal);
|
|
80
|
+
if (detail === null)
|
|
81
|
+
return null;
|
|
82
|
+
return { detail };
|
|
83
|
+
}
|
|
84
|
+
internalDetail(snapshot, id, itemId, query) {
|
|
85
|
+
const versioned = snapshot.sessions.get(id);
|
|
86
|
+
if (versioned === undefined)
|
|
87
|
+
return null;
|
|
88
|
+
const boundary = this.#resolveReadBoundary(id, versioned, query.cursor);
|
|
89
|
+
const detail = itemDetail(versioned.normalized, itemId, "internal", versioned.normalized.internalDetails, boundary.throughOrdinal);
|
|
90
|
+
if (detail === null)
|
|
91
|
+
return null;
|
|
92
|
+
return { detail };
|
|
93
|
+
}
|
|
94
|
+
#resolveReadBoundary(id, versioned, cursor) {
|
|
95
|
+
const { normalized, timelinePrefixIndex } = versioned;
|
|
96
|
+
if (cursor === undefined)
|
|
97
|
+
return timelinePrefixIndex.boundaryAt(normalized.timeline, 0);
|
|
98
|
+
const cursorResult = this.cursors.decodeTimeline(cursor);
|
|
99
|
+
if (cursorResult.kind === "malformed") {
|
|
100
|
+
throw new RepositoryQueryError("invalid_query", "cursor is malformed");
|
|
101
|
+
}
|
|
102
|
+
if (cursorResult.kind === "untrusted") {
|
|
103
|
+
throw new RepositoryQueryError("timeline_changed", "The timeline cursor is no longer valid; reload this session");
|
|
104
|
+
}
|
|
105
|
+
const decoded = cursorResult.value;
|
|
106
|
+
if (decoded.s !== id)
|
|
107
|
+
throw new RepositoryQueryError("invalid_query", "cursor belongs to another session");
|
|
108
|
+
const boundary = timelinePrefixIndex.boundaryAt(normalized.timeline, decoded.o);
|
|
109
|
+
if (boundary === null ||
|
|
110
|
+
boundary.throughOrdinal !== decoded.o ||
|
|
111
|
+
!timelinePrefixIndex.matches(normalized.timeline, boundary, decoded.p)) {
|
|
112
|
+
throw new RepositoryQueryError("timeline_changed", "The loaded timeline is no longer a prefix of this session");
|
|
113
|
+
}
|
|
114
|
+
return boundary;
|
|
115
|
+
}
|
|
116
|
+
#readContext(id, versioned, boundary) {
|
|
117
|
+
return {
|
|
118
|
+
session: versioned.normalized.session,
|
|
119
|
+
cursor: this.cursors.encodeTimeline(id, boundary.throughOrdinal, boundary.timelinePrefixRevision),
|
|
120
|
+
hasMore: boundary.throughOrdinal < (versioned.normalized.timeline.at(-1)?.ordinal ?? 0),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const itemIndexes = new WeakMap();
|
|
125
|
+
function indexedItem(normalized, itemId) {
|
|
126
|
+
let index = itemIndexes.get(normalized);
|
|
127
|
+
if (!index) {
|
|
128
|
+
index = new Map(normalized.timeline.map((item) => [item.id, item]));
|
|
129
|
+
itemIndexes.set(normalized, index);
|
|
130
|
+
}
|
|
131
|
+
return index.get(itemId);
|
|
132
|
+
}
|
|
133
|
+
function itemDetail(normalized, itemId, kind, details, throughOrdinal) {
|
|
134
|
+
const item = indexedItem(normalized, itemId);
|
|
135
|
+
if (item?.kind !== kind)
|
|
136
|
+
return null;
|
|
137
|
+
const detail = details.get(itemId);
|
|
138
|
+
if (detail === undefined)
|
|
139
|
+
return null;
|
|
140
|
+
if (item.ordinal > throughOrdinal) {
|
|
141
|
+
throw new RepositoryQueryError("invalid_query", "cursor does not confirm the requested item");
|
|
142
|
+
}
|
|
143
|
+
return detail;
|
|
144
|
+
}
|
|
145
|
+
function validateItemQuery(query) {
|
|
146
|
+
if (query.limit !== undefined &&
|
|
147
|
+
(!Number.isInteger(query.limit) || query.limit < 1 || query.limit > MAX_ITEM_LIMIT)) {
|
|
148
|
+
throw new RepositoryQueryError("invalid_query", `limit must be between 1 and ${MAX_ITEM_LIMIT}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=session-queries.js.map
|