@zakstam/codex-local-component 0.2.1 → 0.3.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/README.md +23 -4
- package/dist/app-server/client.d.ts +40 -1
- package/dist/app-server/client.d.ts.map +1 -1
- package/dist/app-server/client.js +56 -3
- package/dist/app-server/client.js.map +1 -1
- package/dist/app-server/index.d.ts +1 -1
- package/dist/app-server/index.d.ts.map +1 -1
- package/dist/app-server/index.js +1 -1
- package/dist/app-server/index.js.map +1 -1
- package/dist/client/index.d.ts +4 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/reasoning.d.ts +10 -0
- package/dist/client/reasoning.d.ts.map +1 -0
- package/dist/client/reasoning.js +4 -0
- package/dist/client/reasoning.js.map +1 -0
- package/dist/client/serverRequests.d.ts +14 -0
- package/dist/client/serverRequests.d.ts.map +1 -0
- package/dist/client/serverRequests.js +10 -0
- package/dist/client/serverRequests.js.map +1 -0
- package/dist/client/threads.d.ts +39 -3
- package/dist/client/threads.d.ts.map +1 -1
- package/dist/client/threads.js +18 -0
- package/dist/client/threads.js.map +1 -1
- package/dist/client/types.d.ts +23 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/approvals.d.ts +1 -1
- package/dist/component/index.d.ts +2 -0
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +2 -0
- package/dist/component/index.js.map +1 -1
- package/dist/component/ingest/applyApprovals.d.ts +5 -0
- package/dist/component/ingest/applyApprovals.d.ts.map +1 -0
- package/dist/component/ingest/applyApprovals.js +59 -0
- package/dist/component/ingest/applyApprovals.js.map +1 -0
- package/dist/component/ingest/applyMessages.d.ts +4 -0
- package/dist/component/ingest/applyMessages.d.ts.map +1 -0
- package/dist/component/ingest/applyMessages.js +149 -0
- package/dist/component/ingest/applyMessages.js.map +1 -0
- package/dist/component/ingest/applyStreams.d.ts +7 -0
- package/dist/component/ingest/applyStreams.d.ts.map +1 -0
- package/dist/component/ingest/applyStreams.js +189 -0
- package/dist/component/ingest/applyStreams.js.map +1 -0
- package/dist/component/ingest/applyTurns.d.ts +6 -0
- package/dist/component/ingest/applyTurns.d.ts.map +1 -0
- package/dist/component/ingest/applyTurns.js +94 -0
- package/dist/component/ingest/applyTurns.js.map +1 -0
- package/dist/component/ingest/checkpoints.d.ts +12 -0
- package/dist/component/ingest/checkpoints.d.ts.map +1 -0
- package/dist/component/ingest/checkpoints.js +63 -0
- package/dist/component/ingest/checkpoints.js.map +1 -0
- package/dist/component/ingest/index.d.ts +10 -0
- package/dist/component/ingest/index.d.ts.map +1 -0
- package/dist/component/ingest/index.js +76 -0
- package/dist/component/ingest/index.js.map +1 -0
- package/dist/component/ingest/normalize.d.ts +5 -0
- package/dist/component/ingest/normalize.d.ts.map +1 -0
- package/dist/component/ingest/normalize.js +35 -0
- package/dist/component/ingest/normalize.js.map +1 -0
- package/dist/component/ingest/postIngest.d.ts +4 -0
- package/dist/component/ingest/postIngest.d.ts.map +1 -0
- package/dist/component/ingest/postIngest.js +31 -0
- package/dist/component/ingest/postIngest.js.map +1 -0
- package/dist/component/ingest/sessionGuard.d.ts +9 -0
- package/dist/component/ingest/sessionGuard.d.ts.map +1 -0
- package/dist/component/ingest/sessionGuard.js +102 -0
- package/dist/component/ingest/sessionGuard.js.map +1 -0
- package/dist/component/ingest/stateCache.d.ts +19 -0
- package/dist/component/ingest/stateCache.d.ts.map +1 -0
- package/dist/component/ingest/stateCache.js +121 -0
- package/dist/component/ingest/stateCache.js.map +1 -0
- package/dist/component/ingest/types.d.ts +129 -0
- package/dist/component/ingest/types.d.ts.map +1 -0
- package/dist/component/ingest/types.js +2 -0
- package/dist/component/ingest/types.js.map +1 -0
- package/dist/component/reasoning.d.ts +37 -0
- package/dist/component/reasoning.d.ts.map +1 -0
- package/dist/component/reasoning.js +48 -0
- package/dist/component/reasoning.js.map +1 -0
- package/dist/component/schema.d.ts +87 -11
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +47 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/serverRequests.d.ts +53 -0
- package/dist/component/serverRequests.d.ts.map +1 -0
- package/dist/component/serverRequests.js +187 -0
- package/dist/component/serverRequests.js.map +1 -0
- package/dist/component/streamStats.d.ts +10 -0
- package/dist/component/streamStats.d.ts.map +1 -1
- package/dist/component/streamStats.js +34 -0
- package/dist/component/streamStats.js.map +1 -1
- package/dist/component/sync.d.ts +4 -67
- package/dist/component/sync.d.ts.map +1 -1
- package/dist/component/syncHelpers.d.ts +12 -35
- package/dist/component/syncHelpers.d.ts.map +1 -1
- package/dist/component/syncHelpers.js +11 -228
- package/dist/component/syncHelpers.js.map +1 -1
- package/dist/component/syncIngest.d.ts +2 -72
- package/dist/component/syncIngest.d.ts.map +1 -1
- package/dist/component/syncIngest.js +9 -726
- package/dist/component/syncIngest.js.map +1 -1
- package/dist/component/syncRuntime.d.ts +7 -1
- package/dist/component/syncRuntime.d.ts.map +1 -1
- package/dist/component/syncRuntime.js +8 -2
- package/dist/component/syncRuntime.js.map +1 -1
- package/dist/component/types.d.ts +5 -1
- package/dist/component/types.d.ts.map +1 -1
- package/dist/component/types.js +2 -0
- package/dist/component/types.js.map +1 -1
- package/dist/host/convex-entry.d.ts +3 -0
- package/dist/host/convex-entry.d.ts.map +1 -0
- package/dist/host/convex-entry.js +3 -0
- package/dist/host/convex-entry.js.map +1 -0
- package/dist/host/convex.d.ts +17 -0
- package/dist/host/convex.d.ts.map +1 -1
- package/dist/host/convex.js +9 -0
- package/dist/host/convex.js.map +1 -1
- package/dist/host/convexSlice.d.ts +504 -0
- package/dist/host/convexSlice.d.ts.map +1 -0
- package/dist/host/convexSlice.js +315 -0
- package/dist/host/convexSlice.js.map +1 -0
- package/dist/host/index.d.ts +3 -2
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +2 -1
- package/dist/host/index.js.map +1 -1
- package/dist/host/runtime.d.ts +100 -2
- package/dist/host/runtime.d.ts.map +1 -1
- package/dist/host/runtime.js +329 -33
- package/dist/host/runtime.js.map +1 -1
- package/dist/local-adapter/bridge.d.ts +3 -2
- package/dist/local-adapter/bridge.d.ts.map +1 -1
- package/dist/local-adapter/bridge.js.map +1 -1
- package/dist/mapping.d.ts +29 -0
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +136 -46
- package/dist/mapping.js.map +1 -1
- package/dist/protocol/classifier.d.ts +2 -12
- package/dist/protocol/classifier.d.ts.map +1 -1
- package/dist/protocol/classifier.js +1 -104
- package/dist/protocol/classifier.js.map +1 -1
- package/dist/protocol/events.d.ts +72 -0
- package/dist/protocol/events.d.ts.map +1 -0
- package/dist/protocol/events.js +533 -0
- package/dist/protocol/events.js.map +1 -0
- package/dist/protocol/generated.d.ts +16 -2
- package/dist/protocol/generated.d.ts.map +1 -1
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/index.js.map +1 -1
- package/dist/protocol/outbound.d.ts +13 -0
- package/dist/protocol/outbound.d.ts.map +1 -0
- package/dist/protocol/outbound.js +2 -0
- package/dist/protocol/outbound.js.map +1 -0
- package/dist/protocol/parser.d.ts +3 -2
- package/dist/protocol/parser.d.ts.map +1 -1
- package/dist/protocol/parser.js +99 -3
- package/dist/protocol/parser.js.map +1 -1
- package/dist/protocol/schemas/CommandExecutionRequestApprovalResponse.json +72 -0
- package/dist/protocol/schemas/DynamicToolCallResponse.json +66 -0
- package/dist/protocol/schemas/FileChangeRequestApprovalResponse.json +47 -0
- package/dist/protocol/schemas/ToolRequestUserInputResponse.json +34 -0
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/types.d.ts +13 -1
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useCodexReasoning.d.ts +7 -0
- package/dist/react/useCodexReasoning.d.ts.map +1 -0
- package/dist/react/useCodexReasoning.js +16 -0
- package/dist/react/useCodexReasoning.js.map +1 -0
- package/dist/react/useCodexStreamOverlay.d.ts.map +1 -1
- package/dist/react/useCodexStreamOverlay.js +68 -23
- package/dist/react/useCodexStreamOverlay.js.map +1 -1
- package/dist/react/useCodexStreamingReasoning.d.ts +12 -0
- package/dist/react/useCodexStreamingReasoning.d.ts.map +1 -0
- package/dist/react/useCodexStreamingReasoning.js +21 -0
- package/dist/react/useCodexStreamingReasoning.js.map +1 -0
- package/package.json +5 -1
|
@@ -1,727 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { CLEANUP_SWEEP_MIN_INTERVAL_MS, DELTA_EVENT_KINDS, DELTA_TTL_MS, DEFAULT_STREAM_DELETE_BATCH_SIZE, HEARTBEAT_WRITE_MIN_INTERVAL_MS, LIFECYCLE_EVENT_KINDS, STALE_SWEEP_MIN_INTERVAL_MS, resolveRuntimeOptions, syncError, } from "./syncRuntime.js";
|
|
5
|
-
import { addStreamDeltaStats, ensureStreamStat, setStreamStatState, } from "./streamStats.js";
|
|
6
|
-
const RECOVERABLE_INGEST_CODES = new Set([
|
|
7
|
-
"E_SYNC_SESSION_NOT_FOUND",
|
|
8
|
-
"E_SYNC_SESSION_THREAD_MISMATCH",
|
|
9
|
-
"E_SYNC_SESSION_DEVICE_MISMATCH",
|
|
10
|
-
]);
|
|
11
|
-
function getErrorMessage(error) {
|
|
12
|
-
return error instanceof Error ? error.message : String(error);
|
|
13
|
-
}
|
|
14
|
-
function parseSyncErrorCode(error) {
|
|
15
|
-
const message = getErrorMessage(error);
|
|
16
|
-
const match = /^\[([A-Z0-9_]+)\]/.exec(message);
|
|
17
|
-
return match?.[1] ?? null;
|
|
18
|
-
}
|
|
19
|
-
function mapIngestSafeCode(rawCode) {
|
|
20
|
-
switch (rawCode) {
|
|
21
|
-
case "E_SYNC_SESSION_NOT_FOUND":
|
|
22
|
-
return "SESSION_NOT_FOUND";
|
|
23
|
-
case "E_SYNC_SESSION_THREAD_MISMATCH":
|
|
24
|
-
return "SESSION_THREAD_MISMATCH";
|
|
25
|
-
case "E_SYNC_SESSION_DEVICE_MISMATCH":
|
|
26
|
-
return "SESSION_DEVICE_MISMATCH";
|
|
27
|
-
case "E_SYNC_OUT_OF_ORDER":
|
|
28
|
-
return "OUT_OF_ORDER";
|
|
29
|
-
case "E_SYNC_REPLAY_GAP":
|
|
30
|
-
return "REPLAY_GAP";
|
|
31
|
-
default:
|
|
32
|
-
return "UNKNOWN";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function syntheticTurnStatusForEvent(kind, payloadJson) {
|
|
36
|
-
const terminal = terminalStatusForEvent(kind, payloadJson);
|
|
37
|
-
if (terminal?.status === "completed") {
|
|
38
|
-
return "completed";
|
|
39
|
-
}
|
|
40
|
-
if (terminal?.status === "interrupted") {
|
|
41
|
-
return "interrupted";
|
|
42
|
-
}
|
|
43
|
-
if (terminal?.status === "failed") {
|
|
44
|
-
return "failed";
|
|
45
|
-
}
|
|
46
|
-
if (kind === "turn/started" || kind.startsWith("item/")) {
|
|
47
|
-
return "inProgress";
|
|
48
|
-
}
|
|
49
|
-
return "queued";
|
|
50
|
-
}
|
|
1
|
+
import { ingestEvents } from "./ingest/index.js";
|
|
2
|
+
import { upsertCheckpoint } from "./ingest/checkpoints.js";
|
|
3
|
+
import { errorMessage, isRecoverableIngestErrorCode, mapIngestSafeCode, parseSyncErrorCode, upsertSessionHeartbeat, } from "./ingest/sessionGuard.js";
|
|
51
4
|
export async function ingestHandler(ctx, args) {
|
|
52
|
-
|
|
53
|
-
const runtime = resolveRuntimeOptions(args.runtime);
|
|
54
|
-
const deltas = [...args.streamDeltas, ...args.lifecycleEvents];
|
|
55
|
-
deltas.sort((a, b) => a.createdAt - b.createdAt);
|
|
56
|
-
if (deltas.length === 0) {
|
|
57
|
-
syncError("E_SYNC_EMPTY_BATCH", "ingest received an empty delta batch");
|
|
58
|
-
}
|
|
59
|
-
const session = await ctx.db
|
|
60
|
-
.query("codex_sessions")
|
|
61
|
-
.withIndex("tenantId_sessionId", (q) => q.eq("tenantId", args.actor.tenantId).eq("sessionId", args.sessionId))
|
|
62
|
-
.first();
|
|
63
|
-
if (!session) {
|
|
64
|
-
syncError("E_SYNC_SESSION_NOT_FOUND", `No active session found for sessionId=${args.sessionId}`);
|
|
65
|
-
}
|
|
66
|
-
if (session.threadId !== args.threadId) {
|
|
67
|
-
syncError("E_SYNC_SESSION_THREAD_MISMATCH", `Session threadId=${session.threadId} does not match request threadId=${args.threadId}`);
|
|
68
|
-
}
|
|
69
|
-
if (session.deviceId !== args.actor.deviceId) {
|
|
70
|
-
syncError("E_SYNC_SESSION_DEVICE_MISMATCH", `Session deviceId=${session.deviceId} does not match actor deviceId=${args.actor.deviceId}`);
|
|
71
|
-
}
|
|
72
|
-
if (session.userId !== args.actor.userId) {
|
|
73
|
-
authzError("E_AUTH_SESSION_FORBIDDEN", `User ${args.actor.userId} is not allowed to access session ${args.sessionId}`);
|
|
74
|
-
}
|
|
75
|
-
let lastPersistedCursor = session.lastEventCursor;
|
|
76
|
-
let persistedAnyEvent = false;
|
|
77
|
-
const inBatchEventIds = new Set();
|
|
78
|
-
const knownTurnIds = new Set();
|
|
79
|
-
const startedTurns = new Set();
|
|
80
|
-
const terminalTurns = new Map();
|
|
81
|
-
const pendingApprovals = new Map();
|
|
82
|
-
const resolvedApprovals = new Map();
|
|
83
|
-
const messageOrderCacheByTurn = new Map();
|
|
84
|
-
const messageByKey = new Map();
|
|
85
|
-
const streamById = new Map();
|
|
86
|
-
const approvalByKey = new Map();
|
|
87
|
-
const persistedStatsByStreamId = new Map();
|
|
88
|
-
const streamCheckpointCursorByStreamId = new Map();
|
|
89
|
-
let ingestStatus = "ok";
|
|
90
|
-
const streamStats = await ctx.db
|
|
91
|
-
.query("codex_stream_stats")
|
|
92
|
-
.withIndex("tenantId_threadId", (q) => q.eq("tenantId", args.actor.tenantId).eq("threadId", args.threadId))
|
|
93
|
-
.take(500);
|
|
94
|
-
const expectedCursorByStreamId = new Map(streamStats.map((stat) => [String(stat.streamId), Number(stat.latestCursor)]));
|
|
95
|
-
const nextOrderForTurn = async (turnId) => {
|
|
96
|
-
const cached = messageOrderCacheByTurn.get(turnId);
|
|
97
|
-
if (cached !== undefined) {
|
|
98
|
-
messageOrderCacheByTurn.set(turnId, cached + 1);
|
|
99
|
-
return cached;
|
|
100
|
-
}
|
|
101
|
-
const lastMessage = await ctx.db
|
|
102
|
-
.query("codex_messages")
|
|
103
|
-
.withIndex("tenantId_threadId_turnId_orderInTurn", (q) => q
|
|
104
|
-
.eq("tenantId", args.actor.tenantId)
|
|
105
|
-
.eq("threadId", args.threadId)
|
|
106
|
-
.eq("turnId", turnId))
|
|
107
|
-
.order("desc")
|
|
108
|
-
.take(1);
|
|
109
|
-
const next = (lastMessage[0]?.orderInTurn ?? -1) + 1;
|
|
110
|
-
messageOrderCacheByTurn.set(turnId, next + 1);
|
|
111
|
-
return next;
|
|
112
|
-
};
|
|
113
|
-
const messageKey = (turnId, messageId) => `${args.actor.tenantId}:${args.threadId}:${turnId}:${messageId}`;
|
|
114
|
-
const approvalKey = (turnId, itemId) => `${args.actor.tenantId}:${args.threadId}:${turnId}:${itemId}`;
|
|
115
|
-
const getMessageRecord = async (turnId, messageId) => {
|
|
116
|
-
const key = messageKey(turnId, messageId);
|
|
117
|
-
if (messageByKey.has(key)) {
|
|
118
|
-
return messageByKey.get(key);
|
|
119
|
-
}
|
|
120
|
-
const existing = await ctx.db
|
|
121
|
-
.query("codex_messages")
|
|
122
|
-
.withIndex("tenantId_threadId_turnId_messageId", (q) => q
|
|
123
|
-
.eq("tenantId", args.actor.tenantId)
|
|
124
|
-
.eq("threadId", args.threadId)
|
|
125
|
-
.eq("turnId", turnId)
|
|
126
|
-
.eq("messageId", messageId))
|
|
127
|
-
.first();
|
|
128
|
-
const normalized = existing
|
|
129
|
-
? {
|
|
130
|
-
_id: existing._id,
|
|
131
|
-
status: existing.status,
|
|
132
|
-
text: existing.text,
|
|
133
|
-
}
|
|
134
|
-
: null;
|
|
135
|
-
messageByKey.set(key, normalized);
|
|
136
|
-
return normalized;
|
|
137
|
-
};
|
|
138
|
-
const setMessageRecord = (turnId, messageId, value) => {
|
|
139
|
-
messageByKey.set(messageKey(turnId, messageId), value);
|
|
140
|
-
};
|
|
141
|
-
const getApprovalRecord = async (turnId, itemId) => {
|
|
142
|
-
const key = approvalKey(turnId, itemId);
|
|
143
|
-
if (approvalByKey.has(key)) {
|
|
144
|
-
return approvalByKey.get(key);
|
|
145
|
-
}
|
|
146
|
-
const existing = await ctx.db
|
|
147
|
-
.query("codex_approvals")
|
|
148
|
-
.withIndex("tenantId_threadId_turnId_itemId", (q) => q
|
|
149
|
-
.eq("tenantId", args.actor.tenantId)
|
|
150
|
-
.eq("threadId", args.threadId)
|
|
151
|
-
.eq("turnId", turnId)
|
|
152
|
-
.eq("itemId", itemId))
|
|
153
|
-
.first();
|
|
154
|
-
const normalized = existing
|
|
155
|
-
? {
|
|
156
|
-
_id: existing._id,
|
|
157
|
-
status: existing.status,
|
|
158
|
-
}
|
|
159
|
-
: null;
|
|
160
|
-
approvalByKey.set(key, normalized);
|
|
161
|
-
return normalized;
|
|
162
|
-
};
|
|
163
|
-
const setApprovalRecord = (turnId, itemId, value) => {
|
|
164
|
-
approvalByKey.set(approvalKey(turnId, itemId), value);
|
|
165
|
-
};
|
|
166
|
-
const getStreamRecord = async (streamId) => {
|
|
167
|
-
if (streamById.has(streamId)) {
|
|
168
|
-
return streamById.get(streamId);
|
|
169
|
-
}
|
|
170
|
-
const stream = await ctx.db
|
|
171
|
-
.query("codex_streams")
|
|
172
|
-
.withIndex("tenantId_streamId", (q) => q.eq("tenantId", args.actor.tenantId).eq("streamId", streamId))
|
|
173
|
-
.first();
|
|
174
|
-
const normalized = stream
|
|
175
|
-
? {
|
|
176
|
-
_id: stream._id,
|
|
177
|
-
turnId: stream.turnId,
|
|
178
|
-
state: { kind: stream.state.kind },
|
|
179
|
-
}
|
|
180
|
-
: null;
|
|
181
|
-
streamById.set(streamId, normalized);
|
|
182
|
-
return normalized;
|
|
183
|
-
};
|
|
184
|
-
const setStreamRecord = (streamId, value) => {
|
|
185
|
-
streamById.set(streamId, value);
|
|
186
|
-
};
|
|
187
|
-
for (const delta of deltas) {
|
|
188
|
-
const turnId = delta.turnId;
|
|
189
|
-
if (turnId && !knownTurnIds.has(turnId)) {
|
|
190
|
-
const existingTurn = await ctx.db
|
|
191
|
-
.query("codex_turns")
|
|
192
|
-
.withIndex("tenantId_threadId_turnId", (q) => q
|
|
193
|
-
.eq("tenantId", args.actor.tenantId)
|
|
194
|
-
.eq("threadId", args.threadId)
|
|
195
|
-
.eq("turnId", turnId))
|
|
196
|
-
.first();
|
|
197
|
-
if (!existingTurn) {
|
|
198
|
-
const syntheticStatus = syntheticTurnStatusForEvent(delta.kind, delta.payloadJson);
|
|
199
|
-
await ctx.db.insert("codex_turns", {
|
|
200
|
-
tenantId: args.actor.tenantId,
|
|
201
|
-
userId: args.actor.userId,
|
|
202
|
-
threadId: args.threadId,
|
|
203
|
-
turnId,
|
|
204
|
-
status: syntheticStatus,
|
|
205
|
-
idempotencyKey: `sync:${args.threadId}:${turnId}`,
|
|
206
|
-
startedAt: now(),
|
|
207
|
-
...(syntheticStatus === "completed" ||
|
|
208
|
-
syntheticStatus === "interrupted" ||
|
|
209
|
-
syntheticStatus === "failed"
|
|
210
|
-
? { completedAt: now() }
|
|
211
|
-
: {}),
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
else if (existingTurn.userId !== args.actor.userId) {
|
|
215
|
-
authzError("E_AUTH_TURN_FORBIDDEN", `User ${args.actor.userId} is not allowed to access turn ${turnId}`);
|
|
216
|
-
}
|
|
217
|
-
knownTurnIds.add(turnId);
|
|
218
|
-
}
|
|
219
|
-
if (turnId && delta.kind === "turn/started") {
|
|
220
|
-
startedTurns.add(turnId);
|
|
221
|
-
}
|
|
222
|
-
if (turnId) {
|
|
223
|
-
const terminal = terminalStatusForEvent(delta.kind, delta.payloadJson);
|
|
224
|
-
if (terminal) {
|
|
225
|
-
const current = terminalTurns.get(turnId);
|
|
226
|
-
terminalTurns.set(turnId, pickHigherPriorityTerminalStatus(current, terminal));
|
|
227
|
-
}
|
|
228
|
-
const approvalRequest = parseApprovalRequest(delta.kind, delta.payloadJson);
|
|
229
|
-
if (approvalRequest) {
|
|
230
|
-
pendingApprovals.set(`${turnId}:${approvalRequest.itemId}`, approvalRequest);
|
|
231
|
-
}
|
|
232
|
-
const approvalResolution = parseApprovalResolution(delta.kind, delta.payloadJson);
|
|
233
|
-
if (approvalResolution) {
|
|
234
|
-
resolvedApprovals.set(`${turnId}:${approvalResolution.itemId}`, approvalResolution);
|
|
235
|
-
}
|
|
236
|
-
const durableMessage = parseDurableMessageEvent(delta.kind, delta.payloadJson);
|
|
237
|
-
if (durableMessage) {
|
|
238
|
-
const existing = await getMessageRecord(turnId, durableMessage.messageId);
|
|
239
|
-
if (!existing) {
|
|
240
|
-
const nextOrder = await nextOrderForTurn(turnId);
|
|
241
|
-
const newId = await ctx.db.insert("codex_messages", {
|
|
242
|
-
tenantId: args.actor.tenantId,
|
|
243
|
-
userId: args.actor.userId,
|
|
244
|
-
threadId: args.threadId,
|
|
245
|
-
turnId,
|
|
246
|
-
messageId: durableMessage.messageId,
|
|
247
|
-
role: durableMessage.role,
|
|
248
|
-
status: durableMessage.status,
|
|
249
|
-
text: durableMessage.text,
|
|
250
|
-
sourceItemType: durableMessage.sourceItemType,
|
|
251
|
-
orderInTurn: nextOrder,
|
|
252
|
-
payloadJson: durableMessage.payloadJson,
|
|
253
|
-
...(durableMessage.status === "failed" ? { error: "item failed" } : {}),
|
|
254
|
-
createdAt: delta.createdAt,
|
|
255
|
-
updatedAt: now(),
|
|
256
|
-
...(durableMessage.status !== "streaming" ? { completedAt: now() } : {}),
|
|
257
|
-
});
|
|
258
|
-
setMessageRecord(turnId, durableMessage.messageId, {
|
|
259
|
-
_id: newId,
|
|
260
|
-
status: durableMessage.status,
|
|
261
|
-
text: durableMessage.text,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
const nextStatus = (() => {
|
|
266
|
-
if (existing.status === "failed") {
|
|
267
|
-
return "failed";
|
|
268
|
-
}
|
|
269
|
-
if (existing.status === "interrupted" && durableMessage.status !== "failed") {
|
|
270
|
-
return "interrupted";
|
|
271
|
-
}
|
|
272
|
-
if (durableMessage.status === "streaming") {
|
|
273
|
-
return existing.status;
|
|
274
|
-
}
|
|
275
|
-
return durableMessage.status;
|
|
276
|
-
})();
|
|
277
|
-
await ctx.db.patch(existing._id, {
|
|
278
|
-
role: durableMessage.role,
|
|
279
|
-
status: nextStatus,
|
|
280
|
-
text: durableMessage.text,
|
|
281
|
-
sourceItemType: durableMessage.sourceItemType,
|
|
282
|
-
payloadJson: durableMessage.payloadJson,
|
|
283
|
-
...(nextStatus === "failed" ? { error: "item failed" } : {}),
|
|
284
|
-
updatedAt: now(),
|
|
285
|
-
...(nextStatus !== "streaming" ? { completedAt: now() } : {}),
|
|
286
|
-
});
|
|
287
|
-
setMessageRecord(turnId, durableMessage.messageId, {
|
|
288
|
-
...existing,
|
|
289
|
-
status: nextStatus,
|
|
290
|
-
text: durableMessage.text,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
const durableDelta = parseDurableMessageDeltaEvent(delta.kind, delta.payloadJson);
|
|
295
|
-
if (durableDelta) {
|
|
296
|
-
const existing = await getMessageRecord(turnId, durableDelta.messageId);
|
|
297
|
-
if (!existing) {
|
|
298
|
-
const nextOrder = await nextOrderForTurn(turnId);
|
|
299
|
-
const messageId = await ctx.db.insert("codex_messages", {
|
|
300
|
-
tenantId: args.actor.tenantId,
|
|
301
|
-
userId: args.actor.userId,
|
|
302
|
-
threadId: args.threadId,
|
|
303
|
-
turnId,
|
|
304
|
-
messageId: durableDelta.messageId,
|
|
305
|
-
role: "assistant",
|
|
306
|
-
status: "streaming",
|
|
307
|
-
text: durableDelta.delta,
|
|
308
|
-
sourceItemType: "agentMessage",
|
|
309
|
-
orderInTurn: nextOrder,
|
|
310
|
-
payloadJson: JSON.stringify({
|
|
311
|
-
type: "agentMessage",
|
|
312
|
-
id: durableDelta.messageId,
|
|
313
|
-
text: durableDelta.delta,
|
|
314
|
-
}),
|
|
315
|
-
createdAt: delta.createdAt,
|
|
316
|
-
updatedAt: now(),
|
|
317
|
-
});
|
|
318
|
-
setMessageRecord(turnId, durableDelta.messageId, {
|
|
319
|
-
_id: messageId,
|
|
320
|
-
status: "streaming",
|
|
321
|
-
text: durableDelta.delta,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
else if (existing.status === "streaming") {
|
|
325
|
-
const nextText = `${existing.text}${durableDelta.delta}`;
|
|
326
|
-
await ctx.db.patch(existing._id, {
|
|
327
|
-
text: nextText,
|
|
328
|
-
payloadJson: JSON.stringify({
|
|
329
|
-
type: "agentMessage",
|
|
330
|
-
id: durableDelta.messageId,
|
|
331
|
-
text: nextText,
|
|
332
|
-
}),
|
|
333
|
-
updatedAt: now(),
|
|
334
|
-
});
|
|
335
|
-
setMessageRecord(turnId, durableDelta.messageId, {
|
|
336
|
-
...existing,
|
|
337
|
-
text: nextText,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (delta.type === "lifecycle_event") {
|
|
343
|
-
const existingLifecycle = await ctx.db
|
|
344
|
-
.query("codex_lifecycle_events")
|
|
345
|
-
.withIndex("tenantId_threadId_eventId", (q) => q
|
|
346
|
-
.eq("tenantId", args.actor.tenantId)
|
|
347
|
-
.eq("threadId", args.threadId)
|
|
348
|
-
.eq("eventId", delta.eventId))
|
|
349
|
-
.first();
|
|
350
|
-
if (!existingLifecycle) {
|
|
351
|
-
await ctx.db.insert("codex_lifecycle_events", {
|
|
352
|
-
tenantId: args.actor.tenantId,
|
|
353
|
-
threadId: args.threadId,
|
|
354
|
-
eventId: delta.eventId,
|
|
355
|
-
kind: delta.kind,
|
|
356
|
-
payloadJson: delta.payloadJson,
|
|
357
|
-
createdAt: delta.createdAt,
|
|
358
|
-
...(delta.turnId ? { turnId: delta.turnId } : {}),
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
const stream = await getStreamRecord(delta.streamId);
|
|
364
|
-
if (!stream) {
|
|
365
|
-
const streamId = await ctx.db.insert("codex_streams", {
|
|
366
|
-
tenantId: args.actor.tenantId,
|
|
367
|
-
threadId: args.threadId,
|
|
368
|
-
turnId: delta.turnId,
|
|
369
|
-
streamId: delta.streamId,
|
|
370
|
-
state: { kind: "streaming", lastHeartbeatAt: now() },
|
|
371
|
-
startedAt: now(),
|
|
372
|
-
});
|
|
373
|
-
setStreamRecord(delta.streamId, {
|
|
374
|
-
_id: streamId,
|
|
375
|
-
state: { kind: "streaming" },
|
|
376
|
-
turnId: delta.turnId,
|
|
377
|
-
});
|
|
378
|
-
await ensureStreamStat(ctx, {
|
|
379
|
-
tenantId: args.actor.tenantId,
|
|
380
|
-
threadId: args.threadId,
|
|
381
|
-
turnId: delta.turnId,
|
|
382
|
-
streamId: delta.streamId,
|
|
383
|
-
state: "streaming",
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (delta.cursorStart >= delta.cursorEnd) {
|
|
387
|
-
syncError("E_SYNC_INVALID_CURSOR_RANGE", `Invalid cursor range start=${delta.cursorStart} end=${delta.cursorEnd} for eventId=${delta.eventId}`);
|
|
388
|
-
}
|
|
389
|
-
const streamExpectedCursor = expectedCursorByStreamId.get(delta.streamId) ?? 0;
|
|
390
|
-
const existingStreamEvent = await ctx.db
|
|
391
|
-
.query("codex_stream_deltas_ttl")
|
|
392
|
-
.withIndex("tenantId_streamId_eventId", (q) => q
|
|
393
|
-
.eq("tenantId", args.actor.tenantId)
|
|
394
|
-
.eq("streamId", delta.streamId)
|
|
395
|
-
.eq("eventId", delta.eventId))
|
|
396
|
-
.first();
|
|
397
|
-
if (existingStreamEvent) {
|
|
398
|
-
expectedCursorByStreamId.set(delta.streamId, Math.max(streamExpectedCursor, Number(existingStreamEvent.cursorEnd)));
|
|
399
|
-
streamCheckpointCursorByStreamId.set(delta.streamId, Math.max(streamCheckpointCursorByStreamId.get(delta.streamId) ?? 0, Number(existingStreamEvent.cursorEnd)));
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
if (delta.cursorStart < streamExpectedCursor) {
|
|
403
|
-
syncError("E_SYNC_OUT_OF_ORDER", `Expected cursorStart>=${streamExpectedCursor} for streamId=${delta.streamId} but got ${delta.cursorStart} for eventId=${delta.eventId}`);
|
|
404
|
-
}
|
|
405
|
-
if (delta.cursorStart > streamExpectedCursor) {
|
|
406
|
-
ingestStatus = "partial";
|
|
407
|
-
}
|
|
408
|
-
if (inBatchEventIds.has(delta.eventId)) {
|
|
409
|
-
syncError("E_SYNC_DUP_EVENT_IN_BATCH", `Duplicate eventId in request batch: ${delta.eventId}`);
|
|
410
|
-
}
|
|
411
|
-
inBatchEventIds.add(delta.eventId);
|
|
412
|
-
const shouldPersist = LIFECYCLE_EVENT_KINDS.has(delta.kind) ||
|
|
413
|
-
(runtime.saveStreamDeltas && DELTA_EVENT_KINDS.has(delta.kind));
|
|
414
|
-
if (shouldPersist) {
|
|
415
|
-
await ctx.db.insert("codex_stream_deltas_ttl", {
|
|
416
|
-
tenantId: args.actor.tenantId,
|
|
417
|
-
streamId: delta.streamId,
|
|
418
|
-
turnId: delta.turnId,
|
|
419
|
-
eventId: delta.eventId,
|
|
420
|
-
cursorStart: delta.cursorStart,
|
|
421
|
-
cursorEnd: delta.cursorEnd,
|
|
422
|
-
kind: delta.kind,
|
|
423
|
-
payloadJson: delta.payloadJson,
|
|
424
|
-
createdAt: delta.createdAt,
|
|
425
|
-
expiresAt: now() + DELTA_TTL_MS,
|
|
426
|
-
});
|
|
427
|
-
const existing = persistedStatsByStreamId.get(delta.streamId);
|
|
428
|
-
if (existing) {
|
|
429
|
-
existing.deltaCount += 1;
|
|
430
|
-
existing.latestCursor = Math.max(existing.latestCursor, delta.cursorEnd);
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
persistedStatsByStreamId.set(delta.streamId, {
|
|
434
|
-
threadId: args.threadId,
|
|
435
|
-
turnId: delta.turnId,
|
|
436
|
-
latestCursor: delta.cursorEnd,
|
|
437
|
-
deltaCount: 1,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
lastPersistedCursor = delta.cursorEnd;
|
|
441
|
-
persistedAnyEvent = true;
|
|
442
|
-
}
|
|
443
|
-
streamCheckpointCursorByStreamId.set(delta.streamId, Math.max(streamCheckpointCursorByStreamId.get(delta.streamId) ?? 0, delta.cursorEnd));
|
|
444
|
-
expectedCursorByStreamId.set(delta.streamId, delta.cursorEnd);
|
|
445
|
-
}
|
|
446
|
-
for (const turnId of startedTurns) {
|
|
447
|
-
const turn = await requireTurnForActor(ctx, args.actor, args.threadId, turnId);
|
|
448
|
-
if (turn.status === "queued") {
|
|
449
|
-
await ctx.db.patch(turn._id, { status: "inProgress" });
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
for (const [turnId, terminal] of terminalTurns) {
|
|
453
|
-
await ctx.scheduler.runAfter(0, makeFunctionReference("turnsInternal:finalizeTurnFromStream"), {
|
|
454
|
-
tenantId: args.actor.tenantId,
|
|
455
|
-
threadId: args.threadId,
|
|
456
|
-
turnId,
|
|
457
|
-
status: terminal.status,
|
|
458
|
-
...(terminal.error ? { error: terminal.error } : {}),
|
|
459
|
-
});
|
|
460
|
-
if (terminal.status === "failed" || terminal.status === "interrupted") {
|
|
461
|
-
const pendingMessages = await ctx.db
|
|
462
|
-
.query("codex_messages")
|
|
463
|
-
.withIndex("tenantId_threadId_turnId_status", (q) => q
|
|
464
|
-
.eq("tenantId", args.actor.tenantId)
|
|
465
|
-
.eq("threadId", args.threadId)
|
|
466
|
-
.eq("turnId", turnId)
|
|
467
|
-
.eq("status", "streaming"))
|
|
468
|
-
.take(500);
|
|
469
|
-
await Promise.all(pendingMessages.map((message) => ctx.db.patch(message._id, {
|
|
470
|
-
status: terminal.status,
|
|
471
|
-
...(terminal.error ? { error: terminal.error } : {}),
|
|
472
|
-
updatedAt: now(),
|
|
473
|
-
completedAt: now(),
|
|
474
|
-
})));
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
for (const [key, approval] of pendingApprovals) {
|
|
478
|
-
const turnId = key.split(":")[0];
|
|
479
|
-
if (!turnId) {
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
const existing = await getApprovalRecord(turnId, approval.itemId);
|
|
483
|
-
if (!existing) {
|
|
484
|
-
const approvalId = await ctx.db.insert("codex_approvals", {
|
|
485
|
-
tenantId: args.actor.tenantId,
|
|
486
|
-
userId: args.actor.userId,
|
|
487
|
-
threadId: args.threadId,
|
|
488
|
-
turnId,
|
|
489
|
-
itemId: approval.itemId,
|
|
490
|
-
kind: approval.kind,
|
|
491
|
-
status: "pending",
|
|
492
|
-
...(approval.reason ? { reason: approval.reason } : {}),
|
|
493
|
-
createdAt: now(),
|
|
494
|
-
});
|
|
495
|
-
setApprovalRecord(turnId, approval.itemId, {
|
|
496
|
-
_id: approvalId,
|
|
497
|
-
status: "pending",
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
for (const [key, resolution] of resolvedApprovals) {
|
|
502
|
-
const turnId = key.split(":")[0];
|
|
503
|
-
if (!turnId) {
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
const existing = await getApprovalRecord(turnId, resolution.itemId);
|
|
507
|
-
if (!existing || existing.status !== "pending") {
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
await ctx.db.patch(existing._id, {
|
|
511
|
-
status: resolution.status,
|
|
512
|
-
decidedBy: "runtime",
|
|
513
|
-
decidedAt: now(),
|
|
514
|
-
});
|
|
515
|
-
setApprovalRecord(turnId, resolution.itemId, {
|
|
516
|
-
...existing,
|
|
517
|
-
status: resolution.status,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
const terminalByStream = new Map();
|
|
521
|
-
for (const delta of deltas) {
|
|
522
|
-
if (delta.type !== "stream_delta") {
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
const terminal = terminalStatusForEvent(delta.kind, delta.payloadJson);
|
|
526
|
-
if (!terminal) {
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
const current = terminalByStream.get(delta.streamId);
|
|
530
|
-
terminalByStream.set(delta.streamId, pickHigherPriorityTerminalStatus(current, terminal));
|
|
531
|
-
}
|
|
532
|
-
for (const [streamId, terminal] of terminalByStream) {
|
|
533
|
-
const stream = await getStreamRecord(streamId);
|
|
534
|
-
if (!stream || stream.state.kind !== "streaming") {
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
const endedAt = now();
|
|
538
|
-
const cleanupFnId = await ctx.scheduler.runAfter(runtime.finishedStreamDeleteDelayMs, makeFunctionReference("streams:cleanupFinishedStream"), {
|
|
539
|
-
tenantId: args.actor.tenantId,
|
|
540
|
-
streamId,
|
|
541
|
-
batchSize: DEFAULT_STREAM_DELETE_BATCH_SIZE,
|
|
542
|
-
});
|
|
543
|
-
if (terminal.status === "completed") {
|
|
544
|
-
await ctx.db.patch(stream._id, {
|
|
545
|
-
state: { kind: "finished", endedAt },
|
|
546
|
-
endedAt,
|
|
547
|
-
cleanupScheduledAt: endedAt,
|
|
548
|
-
cleanupFnId,
|
|
549
|
-
});
|
|
550
|
-
await setStreamStatState(ctx, {
|
|
551
|
-
tenantId: args.actor.tenantId,
|
|
552
|
-
threadId: args.threadId,
|
|
553
|
-
turnId: stream.turnId,
|
|
554
|
-
streamId,
|
|
555
|
-
state: "finished",
|
|
556
|
-
});
|
|
557
|
-
setStreamRecord(streamId, {
|
|
558
|
-
...stream,
|
|
559
|
-
state: { kind: "finished" },
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
else {
|
|
563
|
-
await ctx.db.patch(stream._id, {
|
|
564
|
-
state: {
|
|
565
|
-
kind: "aborted",
|
|
566
|
-
reason: terminal.error ?? terminal.status,
|
|
567
|
-
endedAt,
|
|
568
|
-
},
|
|
569
|
-
endedAt,
|
|
570
|
-
cleanupScheduledAt: endedAt,
|
|
571
|
-
cleanupFnId,
|
|
572
|
-
});
|
|
573
|
-
await setStreamStatState(ctx, {
|
|
574
|
-
tenantId: args.actor.tenantId,
|
|
575
|
-
threadId: args.threadId,
|
|
576
|
-
turnId: stream.turnId,
|
|
577
|
-
streamId,
|
|
578
|
-
state: "aborted",
|
|
579
|
-
});
|
|
580
|
-
setStreamRecord(streamId, {
|
|
581
|
-
...stream,
|
|
582
|
-
state: { kind: "aborted" },
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
for (const [streamId, stats] of persistedStatsByStreamId) {
|
|
587
|
-
await addStreamDeltaStats(ctx, {
|
|
588
|
-
tenantId: args.actor.tenantId,
|
|
589
|
-
threadId: stats.threadId,
|
|
590
|
-
turnId: stats.turnId,
|
|
591
|
-
streamId,
|
|
592
|
-
deltaCount: stats.deltaCount,
|
|
593
|
-
latestCursor: stats.latestCursor,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
const sessionPatch = { status: "active" };
|
|
597
|
-
const nextLastEventCursor = Math.max(session.lastEventCursor, lastPersistedCursor);
|
|
598
|
-
if (nextLastEventCursor !== session.lastEventCursor) {
|
|
599
|
-
sessionPatch.lastEventCursor = nextLastEventCursor;
|
|
600
|
-
}
|
|
601
|
-
const nowMs = now();
|
|
602
|
-
if (persistedAnyEvent || nowMs - session.lastHeartbeatAt >= HEARTBEAT_WRITE_MIN_INTERVAL_MS) {
|
|
603
|
-
sessionPatch.lastHeartbeatAt = nowMs;
|
|
604
|
-
}
|
|
605
|
-
await ctx.db.patch(session._id, sessionPatch);
|
|
606
|
-
const streamCheckpointRows = await ctx.db
|
|
607
|
-
.query("codex_stream_checkpoints")
|
|
608
|
-
.withIndex("tenantId_threadId_deviceId_streamId", (q) => q
|
|
609
|
-
.eq("tenantId", args.actor.tenantId)
|
|
610
|
-
.eq("threadId", args.threadId)
|
|
611
|
-
.eq("deviceId", args.actor.deviceId))
|
|
612
|
-
.take(2000);
|
|
613
|
-
const existingCheckpointByStreamId = new Map(streamCheckpointRows.map((row) => [row.streamId, row]));
|
|
614
|
-
for (const [streamId, cursor] of streamCheckpointCursorByStreamId) {
|
|
615
|
-
const existing = existingCheckpointByStreamId.get(streamId);
|
|
616
|
-
if (existing) {
|
|
617
|
-
if (cursor > Number(existing.ackedCursor)) {
|
|
618
|
-
await ctx.db.patch(existing._id, {
|
|
619
|
-
ackedCursor: cursor,
|
|
620
|
-
updatedAt: now(),
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
await ctx.db.insert("codex_stream_checkpoints", {
|
|
626
|
-
tenantId: args.actor.tenantId,
|
|
627
|
-
userId: args.actor.userId,
|
|
628
|
-
deviceId: args.actor.deviceId,
|
|
629
|
-
threadId: args.threadId,
|
|
630
|
-
streamId,
|
|
631
|
-
ackedCursor: cursor,
|
|
632
|
-
updatedAt: now(),
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
if (nowMs - session.lastHeartbeatAt >= STALE_SWEEP_MIN_INTERVAL_MS) {
|
|
636
|
-
await ctx.scheduler.runAfter(0, makeFunctionReference("sessions:timeoutStaleSessions"), {
|
|
637
|
-
tenantId: args.actor.tenantId,
|
|
638
|
-
staleBeforeMs: nowMs - 1000 * 60 * 3,
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
if (persistedAnyEvent && nowMs - session.lastHeartbeatAt >= CLEANUP_SWEEP_MIN_INTERVAL_MS) {
|
|
642
|
-
await ctx.scheduler.runAfter(0, makeFunctionReference("streams:cleanupExpiredDeltas"), {
|
|
643
|
-
nowMs,
|
|
644
|
-
batchSize: 1000,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
const ackedStreams = Array.from(streamCheckpointCursorByStreamId.entries())
|
|
648
|
-
.map(([streamId, ackCursorEnd]) => ({ streamId, ackCursorEnd }))
|
|
649
|
-
.sort((a, b) => a.streamId.localeCompare(b.streamId));
|
|
650
|
-
return { ackedStreams, ingestStatus };
|
|
5
|
+
return ingestEvents(ctx, args);
|
|
651
6
|
}
|
|
652
7
|
export async function upsertCheckpointHandler(ctx, args) {
|
|
653
|
-
|
|
654
|
-
const existing = await ctx.db
|
|
655
|
-
.query("codex_stream_checkpoints")
|
|
656
|
-
.withIndex("tenantId_threadId_deviceId_streamId", (q) => q
|
|
657
|
-
.eq("tenantId", args.actor.tenantId)
|
|
658
|
-
.eq("threadId", args.threadId)
|
|
659
|
-
.eq("deviceId", args.actor.deviceId)
|
|
660
|
-
.eq("streamId", args.streamId))
|
|
661
|
-
.first();
|
|
662
|
-
if (existing) {
|
|
663
|
-
if (args.cursor > Number(existing.ackedCursor)) {
|
|
664
|
-
await ctx.db.patch(existing._id, {
|
|
665
|
-
ackedCursor: args.cursor,
|
|
666
|
-
updatedAt: now(),
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
return { ok: true };
|
|
670
|
-
}
|
|
671
|
-
await ctx.db.insert("codex_stream_checkpoints", {
|
|
672
|
-
tenantId: args.actor.tenantId,
|
|
673
|
-
userId: args.actor.userId,
|
|
674
|
-
deviceId: args.actor.deviceId,
|
|
675
|
-
threadId: args.threadId,
|
|
676
|
-
streamId: args.streamId,
|
|
677
|
-
ackedCursor: Math.max(0, Math.floor(args.cursor)),
|
|
678
|
-
updatedAt: now(),
|
|
679
|
-
});
|
|
680
|
-
return { ok: true };
|
|
681
|
-
}
|
|
682
|
-
async function upsertSessionHeartbeat(ctx, args) {
|
|
683
|
-
await requireThreadForActor(ctx, args.actor, args.threadId);
|
|
684
|
-
const session = await ctx.db
|
|
685
|
-
.query("codex_sessions")
|
|
686
|
-
.withIndex("tenantId_sessionId", (q) => q.eq("tenantId", args.actor.tenantId).eq("sessionId", args.sessionId))
|
|
687
|
-
.first();
|
|
688
|
-
if (!session) {
|
|
689
|
-
await ctx.db.insert("codex_sessions", {
|
|
690
|
-
tenantId: args.actor.tenantId,
|
|
691
|
-
userId: args.actor.userId,
|
|
692
|
-
deviceId: args.actor.deviceId,
|
|
693
|
-
threadId: args.threadId,
|
|
694
|
-
sessionId: args.sessionId,
|
|
695
|
-
status: "active",
|
|
696
|
-
lastHeartbeatAt: now(),
|
|
697
|
-
lastEventCursor: args.lastEventCursor,
|
|
698
|
-
startedAt: now(),
|
|
699
|
-
});
|
|
700
|
-
return {
|
|
701
|
-
sessionId: args.sessionId,
|
|
702
|
-
threadId: args.threadId,
|
|
703
|
-
status: "created",
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
if (session.userId !== args.actor.userId) {
|
|
707
|
-
authzError("E_AUTH_SESSION_FORBIDDEN", `User ${args.actor.userId} is not allowed to access session ${args.sessionId}`);
|
|
708
|
-
}
|
|
709
|
-
if (session.threadId !== args.threadId) {
|
|
710
|
-
syncError("E_SYNC_SESSION_THREAD_MISMATCH", `Session threadId=${session.threadId} does not match request threadId=${args.threadId}`);
|
|
711
|
-
}
|
|
712
|
-
if (session.deviceId !== args.actor.deviceId) {
|
|
713
|
-
syncError("E_SYNC_SESSION_DEVICE_MISMATCH", `Session deviceId=${session.deviceId} does not match actor deviceId=${args.actor.deviceId}`);
|
|
714
|
-
}
|
|
715
|
-
await ctx.db.patch(session._id, {
|
|
716
|
-
status: "active",
|
|
717
|
-
lastHeartbeatAt: now(),
|
|
718
|
-
lastEventCursor: Math.max(args.lastEventCursor, session.lastEventCursor),
|
|
719
|
-
});
|
|
720
|
-
return {
|
|
721
|
-
sessionId: args.sessionId,
|
|
722
|
-
threadId: args.threadId,
|
|
723
|
-
status: "active",
|
|
724
|
-
};
|
|
8
|
+
return upsertCheckpoint(ctx, args);
|
|
725
9
|
}
|
|
726
10
|
export async function ensureSessionHandler(ctx, args) {
|
|
727
11
|
return upsertSessionHeartbeat(ctx, args);
|
|
@@ -743,9 +27,8 @@ export async function ingestSafeHandler(ctx, args) {
|
|
|
743
27
|
}
|
|
744
28
|
catch (initialError) {
|
|
745
29
|
const initialCode = parseSyncErrorCode(initialError);
|
|
746
|
-
const initialMessage =
|
|
747
|
-
|
|
748
|
-
if (!recoverable) {
|
|
30
|
+
const initialMessage = errorMessage(initialError);
|
|
31
|
+
if (!isRecoverableIngestErrorCode(initialCode)) {
|
|
749
32
|
return {
|
|
750
33
|
status: "rejected",
|
|
751
34
|
ingestStatus: "partial",
|
|
@@ -788,8 +71,8 @@ export async function ingestSafeHandler(ctx, args) {
|
|
|
788
71
|
errors: [
|
|
789
72
|
{
|
|
790
73
|
code: mapIngestSafeCode(retryCode),
|
|
791
|
-
message:
|
|
792
|
-
recoverable:
|
|
74
|
+
message: errorMessage(retryError),
|
|
75
|
+
recoverable: isRecoverableIngestErrorCode(retryCode),
|
|
793
76
|
},
|
|
794
77
|
],
|
|
795
78
|
};
|