Skip to content

Commit ce8f485

Browse files
committed
Merge branch 'fix_event_idempotency' of https://github.com/vr-varad/mosoo into fix_event_idempotency
2 parents ad3fa61 + 74de8d8 commit ce8f485

76 files changed

Lines changed: 1878 additions & 518 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@cloudflare/sandbox": "0.12.3",
1717
"@larksuiteoapi/node-sdk": "^1.66.0",
1818
"@mosoo/ag-ui-session": "workspace:*",
19+
"@mosoo/agent-driver": "workspace:*",
1920
"@mosoo/agent-package": "workspace:*",
2021
"@mosoo/contracts": "workspace:*",
2122
"@mosoo/db": "workspace:*",
@@ -28,7 +29,6 @@
2829
"@mosoo/session-policy": "workspace:*",
2930
"@mosoo/skill-package": "workspace:*",
3031
"@orpc/server": "^1.14.3",
31-
"agent-driver": "workspace:*",
3232
"arktype": "^2.2.0",
3333
"better-auth": "1.6.23",
3434
"cloudflare": "7.0.0",

apps/api/src/modules/agents/application/agent-versioned-config.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isSupportedDriverRuntime } from "@mosoo/agent-driver/runtime";
2+
import type { DriverRuntime } from "@mosoo/agent-driver/runtime";
13
import type { JsonObject } from "@mosoo/contracts";
24
import type { AgentBuiltInToolConfig, AgentEnvironmentConfig } from "@mosoo/contracts/agent";
35
import { classifyAgentConfigChanges } from "@mosoo/contracts/agent-config-change-plan";
@@ -7,8 +9,6 @@ import type {
79
} from "@mosoo/contracts/agent-config-change-plan";
810
import type { AgentId, McpServerId, SkillId } from "@mosoo/id";
911
import { getRuntimeCatalogEntry } from "@mosoo/runtime-catalog";
10-
import { isSupportedDriverRuntime } from "agent-driver/runtime";
11-
import type { DriverRuntime } from "agent-driver/runtime";
1212

1313
import { listEditableAgentSkillReferences } from "./agent-deployment-version.service";
1414
import type { AgentRow } from "./agent-types";

apps/api/src/modules/api-command/application/api-command-enqueue.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { AppDeploymentRunId } from "@mosoo/id";
22

33
import type { ApiBindings } from "../../../platform/cloudflare/worker-types";
44
import { admitApiCommand, enqueueApiCommand } from "./api-command-ledger";
5-
import type { ApiCommandAdmission } from "./api-command-ledger";
5+
import type { ApiCommandAdmission, EnqueueApiCommandInput } from "./api-command-ledger";
66
import type {
77
AppDeploymentRunDispatchCommandPayload,
88
ChannelWorkTriggerCommandPayload,
@@ -74,9 +74,15 @@ export async function admitSessionRunDispatchCommand(
7474
bindings: Pick<ApiBindings, "API_COMMAND_QUEUE" | "DB">,
7575
payload: SessionRunDispatchCommandPayload,
7676
): Promise<ApiCommandAdmission> {
77-
return admitApiCommand(bindings, {
77+
return admitApiCommand(bindings, createSessionRunDispatchApiCommandInput(payload));
78+
}
79+
80+
export function createSessionRunDispatchApiCommandInput(
81+
payload: SessionRunDispatchCommandPayload,
82+
): EnqueueApiCommandInput {
83+
return {
7884
dedupeKey: `session_run_dispatch:${payload.sessionRunId}`,
7985
kind: "session_run_dispatch",
8086
payload,
81-
});
87+
};
8288
}

apps/api/src/modules/api-command/application/api-command-ledger.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export interface EnqueueApiCommandInput {
3030
retryTerminal?: boolean;
3131
}
3232

33+
export interface PreparedApiCommand {
34+
commandId: ApiCommandId;
35+
record: ApiCommandRow;
36+
}
37+
3338
export interface ApiCommandClaim {
3439
attemptCount: number;
3540
commandId: ApiCommandId;
@@ -188,40 +193,51 @@ export async function redriveFailedApiCommandEnqueues(
188193
}
189194
}
190195

191-
export async function admitApiCommand(
192-
bindings: ApiCommandDeliveryBindings,
196+
export function prepareApiCommand(
193197
input: EnqueueApiCommandInput,
194-
): Promise<ApiCommandAdmission> {
195-
const nowMs = currentTimestampMs();
196-
const commandId = createPlatformId<ApiCommandId>();
197-
const dedupeKey = normalizeDedupeKey(input.dedupeKey);
198-
const payloadJson = JSON.stringify(input.payload);
199-
const database = getAppDatabase(bindings.DB);
200-
const insertResult = await database
201-
.insert(apiCommandsTable)
202-
.values({
198+
options: { commandId?: ApiCommandId; timestampMs?: number } = {},
199+
): PreparedApiCommand {
200+
const timestampMs = options.timestampMs ?? currentTimestampMs();
201+
const commandId = options.commandId ?? createPlatformId<ApiCommandId>();
202+
203+
return {
204+
commandId,
205+
record: {
203206
attemptCount: 0,
204207
claimExpiresAt: null,
205208
claimOwner: null,
206209
completedAt: null,
207-
createdAt: nowMs,
208-
dedupeKey,
210+
createdAt: timestampMs,
211+
dedupeKey: normalizeDedupeKey(input.dedupeKey),
209212
id: commandId,
210213
kind: input.kind,
211214
lastErrorCode: API_COMMAND_QUEUE_DELIVERY_PENDING_CODE,
212215
lastErrorMessage: API_COMMAND_QUEUE_DELIVERY_PENDING_MESSAGE,
213-
payloadJson,
216+
payloadJson: JSON.stringify(input.payload),
214217
status: "queued",
215-
updatedAt: nowMs,
216-
})
218+
updatedAt: timestampMs,
219+
},
220+
};
221+
}
222+
223+
export async function admitApiCommand(
224+
bindings: ApiCommandDeliveryBindings,
225+
input: EnqueueApiCommandInput,
226+
): Promise<ApiCommandAdmission> {
227+
const prepared = prepareApiCommand(input);
228+
const database = getAppDatabase(bindings.DB);
229+
230+
const insertResult = await database
231+
.insert(apiCommandsTable)
232+
.values(prepared.record)
217233
.onConflictDoNothing()
218234
.run();
219235

220236
if (getD1ChangeCount(insertResult) > 0) {
221-
return { commandId, kind: input.kind, shouldDeliver: true };
237+
return { commandId: prepared.commandId, kind: input.kind, shouldDeliver: true };
222238
}
223239

224-
const current = await findApiCommandByDedupeKey(bindings.DB, dedupeKey);
240+
const current = await findApiCommandByDedupeKey(bindings.DB, prepared.record.dedupeKey);
225241

226242
if (current === null) {
227243
throw new Error("API command enqueue could not confirm the ledger row.");
@@ -237,9 +253,9 @@ export async function admitApiCommand(
237253
completedAt: null,
238254
lastErrorCode: API_COMMAND_QUEUE_DELIVERY_PENDING_CODE,
239255
lastErrorMessage: API_COMMAND_QUEUE_DELIVERY_PENDING_MESSAGE,
240-
payloadJson,
256+
payloadJson: prepared.record.payloadJson,
241257
status: "queued",
242-
updatedAt: nowMs,
258+
updatedAt: prepared.record.updatedAt,
243259
})
244260
.where(
245261
and(

apps/api/src/modules/public-api/public-api-errors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export function toPublicApiError(error: unknown): PublicApiError | null {
3939
return publicForbidden(error.message);
4040
case API_ERROR_CODE.notFound:
4141
return publicNotFound(error.message);
42+
case API_ERROR_CODE.sessionRunClientRequestDuplicate:
43+
return publicIdempotencyConflict(error.message);
4244
case API_ERROR_CODE.unauthorized:
4345
return publicUnauthenticated(error.message);
4446
default:

apps/api/src/modules/public-api/public-thread-events.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { OpenAiPrivateCitationStreamFilter } from "@mosoo/agent-driver/provider-output";
12
import type {
23
PublicThreadApiListThreadEventsResponse,
34
PublicThreadEventLogEntry,
@@ -12,7 +13,6 @@ import type { SessionProcessEvent } from "@mosoo/contracts/session";
1213
import { sessionEventsTable, sessionMessagesTable } from "@mosoo/db";
1314
import { parsePlatformId } from "@mosoo/id";
1415
import type { RuntimeEventId, SessionId, SessionRunId } from "@mosoo/id";
15-
import { OpenAiPrivateCitationStreamFilter } from "agent-driver/provider-output";
1616
import { and, asc, desc, eq, gt, lt } from "drizzle-orm";
1717
import type { SQL } from "drizzle-orm";
1818

@@ -58,15 +58,64 @@ interface LiveMessageState {
5858

5959
class PublicLiveEventRowProjector {
6060
readonly #messages = new Map<string, LiveMessageState>();
61+
readonly #messageKeysByRun = new Map<string, Set<string>>();
62+
63+
#getMessageRunKey(row: PublicThreadEventProcessRow): string {
64+
return row.run_id ?? "";
65+
}
66+
67+
#getMessageKey(row: PublicThreadEventProcessRow): string {
68+
return `${this.#getMessageRunKey(row)}:${row.process_type}`;
69+
}
70+
71+
#setMessage(row: PublicThreadEventProcessRow, message: LiveMessageState): void {
72+
const key = this.#getMessageKey(row);
73+
const runKey = this.#getMessageRunKey(row);
74+
const runKeys = this.#messageKeysByRun.get(runKey);
75+
76+
this.#messages.set(key, message);
77+
if (runKeys === undefined) {
78+
this.#messageKeysByRun.set(runKey, new Set([key]));
79+
} else {
80+
runKeys.add(key);
81+
}
82+
}
83+
84+
#deleteMessage(row: PublicThreadEventProcessRow): void {
85+
const key = this.#getMessageKey(row);
86+
const runKey = this.#getMessageRunKey(row);
87+
const runKeys = this.#messageKeysByRun.get(runKey);
88+
89+
this.#messages.delete(key);
90+
runKeys?.delete(key);
91+
if (runKeys?.size === 0) {
92+
this.#messageKeysByRun.delete(runKey);
93+
}
94+
}
95+
96+
#deleteRunMessages(runId: SessionRunId | null): void {
97+
const runKey = runId ?? "";
98+
const messageKeys = this.#messageKeysByRun.get(runKey);
99+
100+
if (messageKeys === undefined) {
101+
return;
102+
}
103+
104+
for (const messageKey of messageKeys) {
105+
this.#messages.delete(messageKey);
106+
}
107+
108+
this.#messageKeysByRun.delete(runKey);
109+
}
61110

62111
project(rows: readonly PublicThreadEventProcessRow[]): PublicThreadEventProcessRow[] {
63112
const output: PublicThreadEventProcessRow[] = [];
64113

65114
for (const row of rows) {
66-
const key = `${row.run_id ?? ""}:${row.process_type}`;
115+
const key = this.#getMessageKey(row);
67116

68117
if (row.event_type === "message.started") {
69-
this.#messages.set(key, {
118+
this.#setMessage(row, {
70119
filter: new OpenAiPrivateCitationStreamFilter(),
71120
text: "",
72121
});
@@ -78,7 +127,7 @@ class PublicLiveEventRowProjector {
78127

79128
if (message === undefined) {
80129
message = { filter: new OpenAiPrivateCitationStreamFilter(), text: "" };
81-
this.#messages.set(key, message);
130+
this.#setMessage(row, message);
82131
}
83132

84133
const contentText = message.filter.push(row.content_text).text;
@@ -105,7 +154,7 @@ class PublicLiveEventRowProjector {
105154

106155
if (row.event_type === "message.added") {
107156
const message = this.#messages.get(key);
108-
this.#messages.delete(key);
157+
this.#deleteMessage(row);
109158

110159
if (message !== undefined) {
111160
const snapshotText = sanitizePublicOutput(row.content_text).text;
@@ -132,13 +181,7 @@ class PublicLiveEventRowProjector {
132181
row.event_type === "run.completed" ||
133182
row.event_type === "run.failed"
134183
) {
135-
const runPrefix = `${row.run_id ?? ""}:`;
136-
137-
for (const messageKey of this.#messages.keys()) {
138-
if (messageKey.startsWith(runPrefix)) {
139-
this.#messages.delete(messageKey);
140-
}
141-
}
184+
this.#deleteRunMessages(row.run_id);
142185
}
143186

144187
output.push(row);

apps/api/src/modules/runtime/application/agent-runtime-profile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { getSessionOrganizationPath, getSessionRuntimeStatePath } from "@mosoo/agent-driver/paths";
12
import type { JsonObject } from "@mosoo/contracts";
23
import type { AgentKind, AgentReadiness } from "@mosoo/contracts/agent";
34
import type { AccountId, AgentId, SandboxId, SandboxSessionId, SessionId } from "@mosoo/id";
4-
import { getSessionOrganizationPath, getSessionRuntimeStatePath } from "agent-driver/paths";
55

66
import type {
77
DriverConfigRevision,

apps/api/src/modules/runtime/application/execution-plane/driver-boot-payload-prepared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DriverBootPayload } from "agent-driver/boot";
1+
import type { DriverBootPayload } from "@mosoo/agent-driver/boot";
22

33
export interface DriverBootPayloadPreparedInput {
44
readonly bootPayload: DriverBootPayload;

apps/api/src/modules/runtime/application/session-definition/hydrate-run-context.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getSessionOrganizationPath } from "@mosoo/agent-driver/paths";
12
import type { SessionSummary } from "@mosoo/contracts/session";
23
import type { UserWarning } from "@mosoo/contracts/session-run";
34
import type { ResolvedRunSkill } from "@mosoo/contracts/skill";
@@ -6,7 +7,6 @@ import type { AgentId, PlatformId, AppId, SandboxId, SandboxSessionId, SessionId
67
import { getRuntimeCatalogEntry, getRuntimeCatalogVendorForProvider } from "@mosoo/runtime-catalog";
78
import type { RuntimeCatalogVendor } from "@mosoo/runtime-catalog";
89
import { RUNTIME_DIAGNOSTIC_EVENT } from "@mosoo/runtime-events";
9-
import { getSessionOrganizationPath } from "agent-driver/paths";
1010

1111
import type { ApiBindings } from "../../../../platform/cloudflare/worker-types";
1212
import { validationError } from "../../../../platform/errors";

apps/api/src/modules/runtime/application/session-definition/session-config-trace-event.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {
22
MosooSessionConfigTraceMcpServer,
33
MosooSessionConfigTraceValue,
44
} from "@mosoo/ag-ui-session";
5-
import type { DriverBootMcpServer, DriverBootPayload } from "agent-driver/boot";
5+
import type { DriverBootMcpServer, DriverBootPayload } from "@mosoo/agent-driver/boot";
66

77
function summarizeMcpServer(server: DriverBootMcpServer): MosooSessionConfigTraceMcpServer {
88
return {

0 commit comments

Comments
 (0)