@standardagents/builder 0.26.0 → 0.27.1
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/dist/built-in-routes.js +193 -4
- package/dist/built-in-routes.js.map +1 -1
- package/dist/index.js +580 -48
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +68 -8
- package/dist/runtime.js +580 -48
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1097,6 +1097,25 @@ async function awaitBounded(work, budgetMs, label) {
|
|
|
1097
1097
|
if (timer) clearTimeout(timer);
|
|
1098
1098
|
}
|
|
1099
1099
|
}
|
|
1100
|
+
function chunkCarriesPayload(chunk) {
|
|
1101
|
+
switch (chunk.type) {
|
|
1102
|
+
case "content-delta":
|
|
1103
|
+
case "reasoning-delta":
|
|
1104
|
+
return typeof chunk.delta === "string" && chunk.delta.length > 0;
|
|
1105
|
+
case "tool-call-delta":
|
|
1106
|
+
return typeof chunk.argumentsDelta === "string" && chunk.argumentsDelta.length > 0;
|
|
1107
|
+
case "tool-call-start":
|
|
1108
|
+
case "tool-call-done":
|
|
1109
|
+
case "finish":
|
|
1110
|
+
case "image-done":
|
|
1111
|
+
case "web-search-done":
|
|
1112
|
+
case "provider-tool-done":
|
|
1113
|
+
case "error":
|
|
1114
|
+
return true;
|
|
1115
|
+
default:
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1100
1119
|
function modelSupportsImageInput(modelDef) {
|
|
1101
1120
|
const supportsImages = modelDef.capabilities?.supportsImages;
|
|
1102
1121
|
if (typeof supportsImages === "boolean") {
|
|
@@ -1908,7 +1927,7 @@ var init_LLMRequest = __esm({
|
|
|
1908
1927
|
if (next.done) break;
|
|
1909
1928
|
const chunk = next.value;
|
|
1910
1929
|
state.thread.instance.noteExecutionProgress?.();
|
|
1911
|
-
if (firstChunkAt === 0) {
|
|
1930
|
+
if (firstChunkAt === 0 && chunkCarriesPayload(chunk)) {
|
|
1912
1931
|
firstChunkAt = Date.now();
|
|
1913
1932
|
emitLogPatch({ time_to_first_token_ms: firstChunkAt - requestSentAt });
|
|
1914
1933
|
}
|
|
@@ -13166,6 +13185,38 @@ var init_ThreadStateImpl = __esm({
|
|
|
13166
13185
|
}
|
|
13167
13186
|
await this._threadInstance.setValue(key, value);
|
|
13168
13187
|
}
|
|
13188
|
+
// Account-wide sibling of the thread KV: values scoped to the thread's user,
|
|
13189
|
+
// shared across every thread that user owns (backed by the singleton
|
|
13190
|
+
// DurableAgentBuilder `user_values` table).
|
|
13191
|
+
async getUserValue(key) {
|
|
13192
|
+
const userId = this._metadata.user_id;
|
|
13193
|
+
if (!userId) return null;
|
|
13194
|
+
const stub = this._getAgentBuilderStub();
|
|
13195
|
+
if (typeof stub.getUserValue !== "function") {
|
|
13196
|
+
throw new Error("getUserValue is not supported by this runtime");
|
|
13197
|
+
}
|
|
13198
|
+
return await stub.getUserValue(userId, key);
|
|
13199
|
+
}
|
|
13200
|
+
async setUserValue(key, value) {
|
|
13201
|
+
const userId = this._metadata.user_id;
|
|
13202
|
+
if (!userId) {
|
|
13203
|
+
throw new Error("setUserValue requires a thread with an associated user");
|
|
13204
|
+
}
|
|
13205
|
+
const stub = this._getAgentBuilderStub();
|
|
13206
|
+
if (typeof stub.setUserValue !== "function") {
|
|
13207
|
+
throw new Error("setUserValue is not supported by this runtime");
|
|
13208
|
+
}
|
|
13209
|
+
await stub.setUserValue(userId, key, value);
|
|
13210
|
+
}
|
|
13211
|
+
async listUserValues(options = {}) {
|
|
13212
|
+
const userId = this._metadata.user_id;
|
|
13213
|
+
if (!userId) return { entries: [], total: 0 };
|
|
13214
|
+
const stub = this._getAgentBuilderStub();
|
|
13215
|
+
if (typeof stub.listUserValues !== "function") {
|
|
13216
|
+
throw new Error("listUserValues is not supported by this runtime");
|
|
13217
|
+
}
|
|
13218
|
+
return stub.listUserValues(userId, options);
|
|
13219
|
+
}
|
|
13169
13220
|
// ─────────────────────────────────────────────────────────────────────────
|
|
13170
13221
|
// Client-forwarded tools
|
|
13171
13222
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -13209,7 +13260,7 @@ var init_ThreadStateImpl = __esm({
|
|
|
13209
13260
|
const instance = this._threadInstance;
|
|
13210
13261
|
const toolCallId = this._flowState?.currentToolCall?.id;
|
|
13211
13262
|
const mustPark = request.requestPermission != null && await this.forwardedCallNeedsApproval(request);
|
|
13212
|
-
if (mustPark && toolCallId && this._executionState && typeof instance.dispatchClientTool === "function" && typeof instance.hasBridge === "function" && instance.hasBridge()) {
|
|
13263
|
+
if (mustPark && toolCallId && this._executionState && typeof instance.dispatchClientTool === "function" && typeof instance.hasBridge === "function" && await instance.hasBridge()) {
|
|
13213
13264
|
const side = this._flowState?.currentSide === "b" ? "b" : "a";
|
|
13214
13265
|
await instance.dispatchClientTool(request, toolCallId, side);
|
|
13215
13266
|
this._executionState.stop();
|
|
@@ -23259,8 +23310,38 @@ var migration33 = {
|
|
|
23259
23310
|
}
|
|
23260
23311
|
};
|
|
23261
23312
|
|
|
23313
|
+
// src/durable-objects/migrations/034_add_owner_fencing.ts
|
|
23314
|
+
var migration34 = {
|
|
23315
|
+
version: 34,
|
|
23316
|
+
name: "add_owner_fencing",
|
|
23317
|
+
async up(sql) {
|
|
23318
|
+
sql.exec(`
|
|
23319
|
+
CREATE TABLE IF NOT EXISTS pending_client_calls (
|
|
23320
|
+
tool_call_id TEXT PRIMARY KEY,
|
|
23321
|
+
tool TEXT NOT NULL,
|
|
23322
|
+
args TEXT,
|
|
23323
|
+
request_permission TEXT,
|
|
23324
|
+
risk INTEGER,
|
|
23325
|
+
side TEXT NOT NULL DEFAULT 'a',
|
|
23326
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
23327
|
+
created_at INTEGER NOT NULL
|
|
23328
|
+
)
|
|
23329
|
+
`);
|
|
23330
|
+
const tableInfo = sql.exec(`PRAGMA table_info(pending_client_calls)`).toArray();
|
|
23331
|
+
const hasColumn = (name) => tableInfo.some((col) => col.name === name);
|
|
23332
|
+
if (!hasColumn("dispatched_client_id")) {
|
|
23333
|
+
sql.exec(`ALTER TABLE pending_client_calls ADD COLUMN dispatched_client_id TEXT`);
|
|
23334
|
+
}
|
|
23335
|
+
if (!hasColumn("dispatched_generation")) {
|
|
23336
|
+
sql.exec(
|
|
23337
|
+
`ALTER TABLE pending_client_calls ADD COLUMN dispatched_generation INTEGER NOT NULL DEFAULT 0`
|
|
23338
|
+
);
|
|
23339
|
+
}
|
|
23340
|
+
}
|
|
23341
|
+
};
|
|
23342
|
+
|
|
23262
23343
|
// src/durable-objects/migrations/index.ts
|
|
23263
|
-
var migrations = [migration, migration2, migration3, migration4, migration5, migration6, migration7, migration8, migration9, migration10, migration11, migration12, migration13, migration14, migration15, migration16, migration17, migration18, migration19, migration20, migration21, migration22, migration23, migration24, migration25, migration26, migration27, migration28, migration29, migration30, migration31, migration32, migration33];
|
|
23344
|
+
var migrations = [migration, migration2, migration3, migration4, migration5, migration6, migration7, migration8, migration9, migration10, migration11, migration12, migration13, migration14, migration15, migration16, migration17, migration18, migration19, migration20, migration21, migration22, migration23, migration24, migration25, migration26, migration27, migration28, migration29, migration30, migration31, migration32, migration33, migration34];
|
|
23264
23345
|
var LATEST_SCHEMA_VERSION = migrations.length;
|
|
23265
23346
|
|
|
23266
23347
|
// src/durable-objects/DurableThread.ts
|
|
@@ -24060,6 +24141,61 @@ function isQualifiedName(name) {
|
|
|
24060
24141
|
|
|
24061
24142
|
// src/durable-objects/DurableThread.ts
|
|
24062
24143
|
init_sessionTools();
|
|
24144
|
+
|
|
24145
|
+
// src/durable-objects/bridge-owner.ts
|
|
24146
|
+
var EXECUTION_OWNER_KEY = "execution_owner";
|
|
24147
|
+
var CLAIM_MODES = /* @__PURE__ */ new Set([
|
|
24148
|
+
"none",
|
|
24149
|
+
"if_unowned",
|
|
24150
|
+
"if_stale",
|
|
24151
|
+
"takeover"
|
|
24152
|
+
]);
|
|
24153
|
+
function parseBridgeClientParams(url) {
|
|
24154
|
+
const readParam = (name) => {
|
|
24155
|
+
const raw = url.searchParams.get(name);
|
|
24156
|
+
if (raw == null) return null;
|
|
24157
|
+
const trimmed = raw.trim().slice(0, 256);
|
|
24158
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
24159
|
+
};
|
|
24160
|
+
const clientId = readParam("client_id");
|
|
24161
|
+
const info = {
|
|
24162
|
+
clientId,
|
|
24163
|
+
clientName: readParam("client_name"),
|
|
24164
|
+
clientKind: readParam("client_kind")
|
|
24165
|
+
};
|
|
24166
|
+
const rawClaim = readParam("claim");
|
|
24167
|
+
let claim;
|
|
24168
|
+
if (rawClaim && CLAIM_MODES.has(rawClaim)) {
|
|
24169
|
+
claim = rawClaim;
|
|
24170
|
+
} else {
|
|
24171
|
+
claim = clientId ? "if_unowned" : "none";
|
|
24172
|
+
}
|
|
24173
|
+
return { info, claim };
|
|
24174
|
+
}
|
|
24175
|
+
function resolveBridgeClaim(options) {
|
|
24176
|
+
const { clientId, claim, owner, ownerHasLiveSocket } = options;
|
|
24177
|
+
if (!clientId || claim === "none") return false;
|
|
24178
|
+
if (!owner) return true;
|
|
24179
|
+
if (owner.client_id === clientId) return true;
|
|
24180
|
+
if (claim === "takeover") return true;
|
|
24181
|
+
if (claim === "if_stale") return !ownerHasLiveSocket;
|
|
24182
|
+
return false;
|
|
24183
|
+
}
|
|
24184
|
+
function selectBridgeSocket(candidates, ownerClientId) {
|
|
24185
|
+
if (ownerClientId == null) {
|
|
24186
|
+
return candidates.length > 0 ? candidates[0].socket : null;
|
|
24187
|
+
}
|
|
24188
|
+
let legacyFallback = null;
|
|
24189
|
+
for (const candidate of candidates) {
|
|
24190
|
+
if (candidate.clientId === ownerClientId) return candidate.socket;
|
|
24191
|
+
if (candidate.clientId == null && legacyFallback == null) {
|
|
24192
|
+
legacyFallback = candidate.socket;
|
|
24193
|
+
}
|
|
24194
|
+
}
|
|
24195
|
+
return legacyFallback;
|
|
24196
|
+
}
|
|
24197
|
+
|
|
24198
|
+
// src/durable-objects/DurableThread.ts
|
|
24063
24199
|
init_code_execution();
|
|
24064
24200
|
var DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
24065
24201
|
var BRIDGE_LIVENESS_TIMEOUT_MS = 20 * 1e3;
|
|
@@ -24225,6 +24361,14 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
24225
24361
|
// Last time we heard anything (incl. heartbeat pings) from each bridge socket,
|
|
24226
24362
|
// used to detect a dead/half-open client quickly while a tool is in flight.
|
|
24227
24363
|
bridgeLastSeen = /* @__PURE__ */ new Map();
|
|
24364
|
+
// Identity each bridge socket presented at connect (`client_id` etc.). Legacy
|
|
24365
|
+
// clients present nothing and map to null fields. Restored from the socket
|
|
24366
|
+
// attachment after hibernation.
|
|
24367
|
+
bridgeClientInfo = /* @__PURE__ */ new Map();
|
|
24368
|
+
// In-memory mirror of the thread-KV `execution_owner` record. `undefined`
|
|
24369
|
+
// means "not loaded yet"; `null` means "loaded, no owner". Kept coherent by
|
|
24370
|
+
// setValue() so external KV writes (release/reassign) invalidate it too.
|
|
24371
|
+
executionOwnerCache = void 0;
|
|
24228
24372
|
pendingClientCalls = /* @__PURE__ */ new Map();
|
|
24229
24373
|
alarmQueue;
|
|
24230
24374
|
alarmQueueRearmed = false;
|
|
@@ -24295,6 +24439,11 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
24295
24439
|
} else if (attachment?.type === "bridge") {
|
|
24296
24440
|
this.bridgeSockets.add(ws);
|
|
24297
24441
|
this.bridgeLastSeen.set(ws, Date.now());
|
|
24442
|
+
this.bridgeClientInfo.set(ws, {
|
|
24443
|
+
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
24444
|
+
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
24445
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
24446
|
+
});
|
|
24298
24447
|
}
|
|
24299
24448
|
}
|
|
24300
24449
|
}
|
|
@@ -24788,6 +24937,9 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
24788
24937
|
async setValue(key, value) {
|
|
24789
24938
|
await this.ensureMigrated();
|
|
24790
24939
|
this.assertValidThreadValueKey(key);
|
|
24940
|
+
if (key === EXECUTION_OWNER_KEY) {
|
|
24941
|
+
this.executionOwnerCache = this.parseExecutionOwner(value);
|
|
24942
|
+
}
|
|
24791
24943
|
const updatedAt = Date.now() * 1e3;
|
|
24792
24944
|
if (value === null || value === void 0) {
|
|
24793
24945
|
await this.ctx.storage.sql.exec(`DELETE FROM thread_values WHERE key = ?`, key);
|
|
@@ -25435,9 +25587,9 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
25435
25587
|
* Each migration is run in order, starting from the current version + 1.
|
|
25436
25588
|
*/
|
|
25437
25589
|
async runMigrations(fromVersion) {
|
|
25438
|
-
for (const
|
|
25439
|
-
if (
|
|
25440
|
-
await
|
|
25590
|
+
for (const migration46 of migrations) {
|
|
25591
|
+
if (migration46.version > fromVersion) {
|
|
25592
|
+
await migration46.up(this.ctx.storage.sql);
|
|
25441
25593
|
}
|
|
25442
25594
|
}
|
|
25443
25595
|
}
|
|
@@ -27484,20 +27636,208 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27484
27636
|
async handleBridgeWebSocketUpgrade(request) {
|
|
27485
27637
|
const pair = new WebSocketPair();
|
|
27486
27638
|
const [client, server] = Object.values(pair);
|
|
27639
|
+
const requestUrl2 = new URL(request.url);
|
|
27640
|
+
const { info, claim } = parseBridgeClientParams(requestUrl2);
|
|
27641
|
+
const routeThreadId = requestUrl2.pathname.match(/\/api\/threads\/([^/]+)\//)?.[1] ?? null;
|
|
27487
27642
|
this.ctx.acceptWebSocket(server);
|
|
27488
|
-
server.serializeAttachment({
|
|
27643
|
+
server.serializeAttachment({
|
|
27644
|
+
type: "bridge",
|
|
27645
|
+
clientId: info.clientId,
|
|
27646
|
+
clientName: info.clientName,
|
|
27647
|
+
clientKind: info.clientKind
|
|
27648
|
+
});
|
|
27489
27649
|
this.bridgeSockets.add(server);
|
|
27490
27650
|
this.bridgeLastSeen.set(server, Date.now());
|
|
27651
|
+
this.bridgeClientInfo.set(server, info);
|
|
27652
|
+
if (info.clientId) {
|
|
27653
|
+
for (const ws of Array.from(this.bridgeSockets)) {
|
|
27654
|
+
if (ws === server) continue;
|
|
27655
|
+
if ((this.bridgeClientInfo.get(ws)?.clientId ?? null) !== info.clientId) continue;
|
|
27656
|
+
this.bridgeSockets.delete(ws);
|
|
27657
|
+
this.bridgeLastSeen.delete(ws);
|
|
27658
|
+
this.bridgeClientInfo.delete(ws);
|
|
27659
|
+
try {
|
|
27660
|
+
ws.send(JSON.stringify({ type: "superseded" }));
|
|
27661
|
+
} catch {
|
|
27662
|
+
}
|
|
27663
|
+
try {
|
|
27664
|
+
ws.close(4001, "Superseded by a newer connection from this client");
|
|
27665
|
+
} catch {
|
|
27666
|
+
}
|
|
27667
|
+
}
|
|
27668
|
+
}
|
|
27669
|
+
let owner = await this.getExecutionOwner();
|
|
27670
|
+
let claimRefused = null;
|
|
27671
|
+
const shouldClaim = resolveBridgeClaim({
|
|
27672
|
+
clientId: info.clientId,
|
|
27673
|
+
claim,
|
|
27674
|
+
owner,
|
|
27675
|
+
ownerHasLiveSocket: owner ? this.hasLiveBridgeSocketForClient(owner.client_id, server) : false
|
|
27676
|
+
});
|
|
27677
|
+
if (shouldClaim && info.clientId) {
|
|
27678
|
+
const changed = !owner || owner.client_id !== info.clientId;
|
|
27679
|
+
const ownerLive = owner ? this.hasLiveBridgeSocketForClient(owner.client_id, server) : false;
|
|
27680
|
+
if (changed && ownerLive && await this.hasInFlightClientCalls()) {
|
|
27681
|
+
claimRefused = "in_flight";
|
|
27682
|
+
} else {
|
|
27683
|
+
const previous = owner;
|
|
27684
|
+
const next = {
|
|
27685
|
+
client_id: info.clientId,
|
|
27686
|
+
client_name: info.clientName,
|
|
27687
|
+
client_kind: info.clientKind,
|
|
27688
|
+
claimed_at: Date.now(),
|
|
27689
|
+
generation: changed ? (owner?.generation ?? 0) + 1 : owner?.generation ?? 1
|
|
27690
|
+
};
|
|
27691
|
+
await this.setExecutionOwner(next);
|
|
27692
|
+
owner = next;
|
|
27693
|
+
if (changed) {
|
|
27694
|
+
this.broadcastOwnerChanged(next, server);
|
|
27695
|
+
if (previous) await this.failStalePendingWork(previous, routeThreadId);
|
|
27696
|
+
}
|
|
27697
|
+
}
|
|
27698
|
+
}
|
|
27699
|
+
const isOwner = owner ? info.clientId === owner.client_id : true;
|
|
27491
27700
|
try {
|
|
27492
|
-
server.send(
|
|
27701
|
+
server.send(
|
|
27702
|
+
JSON.stringify({
|
|
27703
|
+
type: "bridge_ready",
|
|
27704
|
+
threadId: this.ctx.id.toString(),
|
|
27705
|
+
owner: isOwner,
|
|
27706
|
+
...claimRefused ? { claim_refused: claimRefused } : {},
|
|
27707
|
+
current_owner: owner ? {
|
|
27708
|
+
client_id: owner.client_id,
|
|
27709
|
+
client_name: owner.client_name ?? null,
|
|
27710
|
+
client_kind: owner.client_kind ?? null,
|
|
27711
|
+
generation: owner.generation
|
|
27712
|
+
} : null
|
|
27713
|
+
})
|
|
27714
|
+
);
|
|
27493
27715
|
} catch {
|
|
27494
27716
|
}
|
|
27495
|
-
|
|
27717
|
+
if (isOwner) {
|
|
27718
|
+
void this.resendPendingClientCalls(server);
|
|
27719
|
+
}
|
|
27496
27720
|
return new Response(null, {
|
|
27497
27721
|
status: 101,
|
|
27498
27722
|
webSocket: client
|
|
27499
27723
|
});
|
|
27500
27724
|
}
|
|
27725
|
+
/** Parse a raw KV value into an {@link ExecutionOwner}, or null. */
|
|
27726
|
+
parseExecutionOwner(value) {
|
|
27727
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
27728
|
+
const record = value;
|
|
27729
|
+
if (typeof record.client_id !== "string" || record.client_id.length === 0) {
|
|
27730
|
+
return null;
|
|
27731
|
+
}
|
|
27732
|
+
return {
|
|
27733
|
+
client_id: record.client_id,
|
|
27734
|
+
client_name: typeof record.client_name === "string" ? record.client_name : null,
|
|
27735
|
+
client_kind: typeof record.client_kind === "string" ? record.client_kind : null,
|
|
27736
|
+
claimed_at: typeof record.claimed_at === "number" ? record.claimed_at : 0,
|
|
27737
|
+
generation: typeof record.generation === "number" ? record.generation : 0
|
|
27738
|
+
};
|
|
27739
|
+
}
|
|
27740
|
+
/** Load (and cache) the thread's persisted execution-owner record. */
|
|
27741
|
+
async getExecutionOwner() {
|
|
27742
|
+
if (this.executionOwnerCache !== void 0) return this.executionOwnerCache;
|
|
27743
|
+
const raw = await this.getValue(EXECUTION_OWNER_KEY);
|
|
27744
|
+
this.executionOwnerCache = this.parseExecutionOwner(raw);
|
|
27745
|
+
return this.executionOwnerCache;
|
|
27746
|
+
}
|
|
27747
|
+
/** Owner id from the in-memory mirror only (for sync paths); null when
|
|
27748
|
+
* unloaded or unowned — both degrade to the historical any-socket rule. */
|
|
27749
|
+
currentExecutionOwnerId() {
|
|
27750
|
+
return this.executionOwnerCache?.client_id ?? null;
|
|
27751
|
+
}
|
|
27752
|
+
/** Persist + cache the execution-owner record (null releases ownership). */
|
|
27753
|
+
async setExecutionOwner(owner) {
|
|
27754
|
+
await this.setValue(EXECUTION_OWNER_KEY, owner);
|
|
27755
|
+
}
|
|
27756
|
+
/** Whether the given client id has a live bridge socket (excluding `except`). */
|
|
27757
|
+
hasLiveBridgeSocketForClient(clientId, except) {
|
|
27758
|
+
const now = Date.now();
|
|
27759
|
+
for (const ws of this.bridgeSockets) {
|
|
27760
|
+
if (ws === except) continue;
|
|
27761
|
+
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
27762
|
+
const lastSeen = this.bridgeLastSeen.get(ws) ?? 0;
|
|
27763
|
+
if (now - lastSeen > BRIDGE_LIVENESS_TIMEOUT_MS) continue;
|
|
27764
|
+
if ((this.bridgeClientInfo.get(ws)?.clientId ?? null) === clientId) return true;
|
|
27765
|
+
}
|
|
27766
|
+
return false;
|
|
27767
|
+
}
|
|
27768
|
+
/** Tell every other bridge socket that execution ownership moved. */
|
|
27769
|
+
broadcastOwnerChanged(owner, except) {
|
|
27770
|
+
const frame = JSON.stringify({
|
|
27771
|
+
type: "owner_changed",
|
|
27772
|
+
owner: {
|
|
27773
|
+
client_id: owner.client_id,
|
|
27774
|
+
client_name: owner.client_name ?? null,
|
|
27775
|
+
client_kind: owner.client_kind ?? null,
|
|
27776
|
+
generation: owner.generation
|
|
27777
|
+
}
|
|
27778
|
+
});
|
|
27779
|
+
for (const ws of this.bridgeSockets) {
|
|
27780
|
+
if (ws === except) continue;
|
|
27781
|
+
try {
|
|
27782
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(frame);
|
|
27783
|
+
} catch {
|
|
27784
|
+
}
|
|
27785
|
+
}
|
|
27786
|
+
}
|
|
27787
|
+
/** Whether any forwarded call is mid-flight (blocking await or parked). */
|
|
27788
|
+
async hasInFlightClientCalls() {
|
|
27789
|
+
if (this.pendingClientCalls.size > 0) return true;
|
|
27790
|
+
await this.ensureMigrated();
|
|
27791
|
+
await this.ensurePendingClientTable();
|
|
27792
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
27793
|
+
`SELECT COUNT(*) AS total FROM pending_client_calls WHERE status = 'pending'`
|
|
27794
|
+
);
|
|
27795
|
+
return Number(cursor.toArray()[0]?.total ?? 0) > 0;
|
|
27796
|
+
}
|
|
27797
|
+
/**
|
|
27798
|
+
* Ownership moved away from a dead client: settle everything that was
|
|
27799
|
+
* dispatched to it. In-memory blocking calls fail immediately; parked
|
|
27800
|
+
* durable calls get an error result and the turn resumes — re-sending them
|
|
27801
|
+
* to the NEW owner would silently re-run the operation on a different
|
|
27802
|
+
* machine's filesystem, which is worse than an honest retryable error.
|
|
27803
|
+
*/
|
|
27804
|
+
async failStalePendingWork(previous, threadId) {
|
|
27805
|
+
for (const id of Array.from(this.pendingClientCalls.keys())) {
|
|
27806
|
+
this.settleClientCall(id, {
|
|
27807
|
+
ok: false,
|
|
27808
|
+
error: "The client executing this operation was replaced before it finished.",
|
|
27809
|
+
data: { noClient: true }
|
|
27810
|
+
});
|
|
27811
|
+
}
|
|
27812
|
+
try {
|
|
27813
|
+
await this.ensureMigrated();
|
|
27814
|
+
await this.ensurePendingClientTable();
|
|
27815
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
27816
|
+
`SELECT tool_call_id FROM pending_client_calls
|
|
27817
|
+
WHERE status = 'pending' AND dispatched_client_id = ?`,
|
|
27818
|
+
previous.client_id
|
|
27819
|
+
);
|
|
27820
|
+
const sides = /* @__PURE__ */ new Set();
|
|
27821
|
+
for (const row of cursor.toArray()) {
|
|
27822
|
+
const delivered = await this.deliverClientToolResult(
|
|
27823
|
+
row.tool_call_id,
|
|
27824
|
+
false,
|
|
27825
|
+
void 0,
|
|
27826
|
+
"The client executing this operation was replaced before it finished. Retry if the work is still needed.",
|
|
27827
|
+
previous.client_id,
|
|
27828
|
+
previous.generation
|
|
27829
|
+
);
|
|
27830
|
+
if (delivered) sides.add(delivered.side);
|
|
27831
|
+
}
|
|
27832
|
+
if (threadId) {
|
|
27833
|
+
for (const side of sides) {
|
|
27834
|
+
await this.continueExecution(threadId, side);
|
|
27835
|
+
}
|
|
27836
|
+
}
|
|
27837
|
+
} catch (error) {
|
|
27838
|
+
console.error("[DurableThread] failStalePendingWork:", error);
|
|
27839
|
+
}
|
|
27840
|
+
}
|
|
27501
27841
|
/**
|
|
27502
27842
|
* Forward a tool operation to a connected bridge client and await its result.
|
|
27503
27843
|
*
|
|
@@ -27506,7 +27846,8 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27506
27846
|
* denial) for the model to reason about.
|
|
27507
27847
|
*/
|
|
27508
27848
|
async callClientTool(request) {
|
|
27509
|
-
const
|
|
27849
|
+
const owner = await this.getExecutionOwner();
|
|
27850
|
+
const socket = this.getOpenBridgeSocket(owner?.client_id ?? null);
|
|
27510
27851
|
if (!socket) {
|
|
27511
27852
|
return {
|
|
27512
27853
|
ok: false,
|
|
@@ -27537,7 +27878,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27537
27878
|
});
|
|
27538
27879
|
}
|
|
27539
27880
|
}, 5e3);
|
|
27540
|
-
this.pendingClientCalls.set(id, { resolve: resolve2, timer, liveness });
|
|
27881
|
+
this.pendingClientCalls.set(id, { resolve: resolve2, timer, liveness, socket });
|
|
27541
27882
|
const frame = JSON.stringify({
|
|
27542
27883
|
type: "tool_request",
|
|
27543
27884
|
id,
|
|
@@ -27571,6 +27912,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27571
27912
|
markBridgeDead(socket) {
|
|
27572
27913
|
this.bridgeSockets.delete(socket);
|
|
27573
27914
|
this.bridgeLastSeen.delete(socket);
|
|
27915
|
+
this.bridgeClientInfo.delete(socket);
|
|
27574
27916
|
try {
|
|
27575
27917
|
socket.close();
|
|
27576
27918
|
} catch {
|
|
@@ -27578,33 +27920,42 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27578
27920
|
this.failPendingClientCallsIfNoBridge();
|
|
27579
27921
|
}
|
|
27580
27922
|
/**
|
|
27581
|
-
* Return the
|
|
27582
|
-
*
|
|
27923
|
+
* Return the bridge socket a forwarded call should be dispatched to, pruning
|
|
27924
|
+
* dead ones. A half-open client (idle NAT/proxy drop) keeps a
|
|
27583
27925
|
* `readyState === OPEN` socket that never fires close/error, so we also treat
|
|
27584
27926
|
* a socket we haven't heard from within {@link BRIDGE_LIVENESS_TIMEOUT_MS} as
|
|
27585
27927
|
* dead. The CLI heartbeats every few seconds, so this only trips on a truly
|
|
27586
27928
|
* gone client — and it means we won't dispatch a forwarded call into the void.
|
|
27929
|
+
*
|
|
27930
|
+
* With an execution owner recorded, only the owner's socket (or, as a
|
|
27931
|
+
* back-compat fallback, an unidentified legacy socket) is eligible; with no
|
|
27932
|
+
* owner, the first live socket wins as it always has.
|
|
27587
27933
|
*/
|
|
27588
|
-
getOpenBridgeSocket() {
|
|
27934
|
+
getOpenBridgeSocket(ownerClientId) {
|
|
27589
27935
|
const now = Date.now();
|
|
27936
|
+
const live = [];
|
|
27590
27937
|
for (const ws of Array.from(this.bridgeSockets)) {
|
|
27591
27938
|
const lastSeen = this.bridgeLastSeen.get(ws) ?? 0;
|
|
27592
27939
|
if (ws.readyState === WebSocket.OPEN && now - lastSeen <= BRIDGE_LIVENESS_TIMEOUT_MS) {
|
|
27593
|
-
|
|
27940
|
+
live.push({ socket: ws, clientId: this.bridgeClientInfo.get(ws)?.clientId ?? null });
|
|
27941
|
+
continue;
|
|
27594
27942
|
}
|
|
27595
27943
|
this.bridgeSockets.delete(ws);
|
|
27596
27944
|
this.bridgeLastSeen.delete(ws);
|
|
27945
|
+
this.bridgeClientInfo.delete(ws);
|
|
27597
27946
|
try {
|
|
27598
27947
|
ws.close();
|
|
27599
27948
|
} catch {
|
|
27600
27949
|
}
|
|
27601
27950
|
}
|
|
27602
|
-
return
|
|
27951
|
+
return selectBridgeSocket(live, ownerClientId);
|
|
27603
27952
|
}
|
|
27604
27953
|
/** Resolve an in-flight forwarded tool call from a client `tool_response`. */
|
|
27605
|
-
resolveClientCall(payload) {
|
|
27954
|
+
resolveClientCall(ws, payload) {
|
|
27606
27955
|
const id = typeof payload.id === "string" ? payload.id : null;
|
|
27607
27956
|
if (!id) return;
|
|
27957
|
+
const pending = this.pendingClientCalls.get(id);
|
|
27958
|
+
if (pending?.socket && pending.socket !== ws) return;
|
|
27608
27959
|
this.settleClientCall(id, {
|
|
27609
27960
|
ok: payload.ok === true,
|
|
27610
27961
|
result: typeof payload.result === "string" ? payload.result : void 0,
|
|
@@ -27613,11 +27964,14 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27613
27964
|
});
|
|
27614
27965
|
}
|
|
27615
27966
|
/**
|
|
27616
|
-
* When the last bridge client
|
|
27617
|
-
*
|
|
27967
|
+
* When the last bridge client that could serve forwarded calls disconnects,
|
|
27968
|
+
* fail any in-flight ones so the agent loop doesn't hang waiting on a client
|
|
27969
|
+
* that is gone. Uses the in-memory owner mirror (sync path); when the mirror
|
|
27970
|
+
* isn't loaded this degrades to the historical any-socket check, and the
|
|
27971
|
+
* per-call liveness interval still catches a dead dispatch target.
|
|
27618
27972
|
*/
|
|
27619
27973
|
failPendingClientCallsIfNoBridge() {
|
|
27620
|
-
if (this.getOpenBridgeSocket()) return;
|
|
27974
|
+
if (this.getOpenBridgeSocket(this.currentExecutionOwnerId())) return;
|
|
27621
27975
|
for (const id of Array.from(this.pendingClientCalls.keys())) {
|
|
27622
27976
|
this.settleClientCall(id, {
|
|
27623
27977
|
ok: false,
|
|
@@ -27626,9 +27980,11 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27626
27980
|
});
|
|
27627
27981
|
}
|
|
27628
27982
|
}
|
|
27629
|
-
/** Whether a bridge client
|
|
27630
|
-
|
|
27631
|
-
|
|
27983
|
+
/** Whether a bridge client that can execute forwarded calls is connected
|
|
27984
|
+
* right now (used to decide durable park). */
|
|
27985
|
+
async hasBridge() {
|
|
27986
|
+
const owner = await this.getExecutionOwner();
|
|
27987
|
+
return this.getOpenBridgeSocket(owner?.client_id ?? null) !== null;
|
|
27632
27988
|
}
|
|
27633
27989
|
// ─── Durable forwarded tool calls (park & resume) ──────────────────────────
|
|
27634
27990
|
// A permission-gated forwarded call must not hold the agent loop open across
|
|
@@ -27645,7 +28001,9 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27645
28001
|
risk INTEGER,
|
|
27646
28002
|
side TEXT NOT NULL DEFAULT 'a',
|
|
27647
28003
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
27648
|
-
created_at INTEGER NOT NULL
|
|
28004
|
+
created_at INTEGER NOT NULL,
|
|
28005
|
+
dispatched_client_id TEXT,
|
|
28006
|
+
dispatched_generation INTEGER NOT NULL DEFAULT 0
|
|
27649
28007
|
)
|
|
27650
28008
|
`);
|
|
27651
28009
|
}
|
|
@@ -27670,13 +28028,31 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27670
28028
|
side,
|
|
27671
28029
|
Date.now()
|
|
27672
28030
|
);
|
|
27673
|
-
return { dispatched: this.sendClientToolRequest(toolCallId, request) };
|
|
28031
|
+
return { dispatched: await this.sendClientToolRequest(toolCallId, request) };
|
|
27674
28032
|
}
|
|
27675
|
-
/**
|
|
27676
|
-
|
|
27677
|
-
|
|
28033
|
+
/**
|
|
28034
|
+
* Send one durable `tool_request` frame to the bridge if a client is
|
|
28035
|
+
* connected — and stamp the pending row with the identity+generation it was
|
|
28036
|
+
* dispatched to, fencing the eventual HTTP delivery to that client. A
|
|
28037
|
+
* legacy (unidentified) executor leaves the row unstamped, keeping the
|
|
28038
|
+
* historical accept-anything delivery for old CLIs.
|
|
28039
|
+
*/
|
|
28040
|
+
async sendClientToolRequest(toolCallId, request) {
|
|
28041
|
+
const owner = await this.getExecutionOwner();
|
|
28042
|
+
const socket = this.getOpenBridgeSocket(owner?.client_id ?? null);
|
|
27678
28043
|
if (!socket) return false;
|
|
28044
|
+
const socketClientId = this.bridgeClientInfo.get(socket)?.clientId ?? null;
|
|
28045
|
+
const fenced = owner && socketClientId === owner.client_id;
|
|
28046
|
+
const generation = fenced ? owner.generation : 0;
|
|
27679
28047
|
try {
|
|
28048
|
+
await this.ctx.storage.sql.exec(
|
|
28049
|
+
`UPDATE pending_client_calls
|
|
28050
|
+
SET dispatched_client_id = ?, dispatched_generation = ?
|
|
28051
|
+
WHERE tool_call_id = ? AND status = 'pending'`,
|
|
28052
|
+
fenced ? owner.client_id : null,
|
|
28053
|
+
generation,
|
|
28054
|
+
toolCallId
|
|
28055
|
+
);
|
|
27680
28056
|
socket.send(
|
|
27681
28057
|
JSON.stringify({
|
|
27682
28058
|
type: "tool_request",
|
|
@@ -27685,7 +28061,8 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27685
28061
|
tool: request.tool,
|
|
27686
28062
|
args: request.args ?? {},
|
|
27687
28063
|
requestPermission: request.requestPermission ?? null,
|
|
27688
|
-
risk: typeof request.risk === "number" ? request.risk : null
|
|
28064
|
+
risk: typeof request.risk === "number" ? request.risk : null,
|
|
28065
|
+
generation
|
|
27689
28066
|
})
|
|
27690
28067
|
);
|
|
27691
28068
|
this.bridgeLastSeen.set(socket, Date.now());
|
|
@@ -27721,9 +28098,21 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27721
28098
|
/** Re-send all pending durable requests to a freshly connected bridge socket. */
|
|
27722
28099
|
async resendPendingClientCalls(socket) {
|
|
27723
28100
|
try {
|
|
28101
|
+
const owner = await this.getExecutionOwner();
|
|
28102
|
+
const socketClientId = this.bridgeClientInfo.get(socket)?.clientId ?? null;
|
|
28103
|
+
const fenced = owner && socketClientId === owner.client_id;
|
|
28104
|
+
const generation = fenced ? owner.generation : 0;
|
|
27724
28105
|
const pending = await this.getPendingClientCalls();
|
|
27725
28106
|
for (const p of pending) {
|
|
27726
28107
|
if (socket.readyState !== WebSocket.OPEN) break;
|
|
28108
|
+
await this.ctx.storage.sql.exec(
|
|
28109
|
+
`UPDATE pending_client_calls
|
|
28110
|
+
SET dispatched_client_id = ?, dispatched_generation = ?
|
|
28111
|
+
WHERE tool_call_id = ? AND status = 'pending'`,
|
|
28112
|
+
fenced ? owner.client_id : null,
|
|
28113
|
+
generation,
|
|
28114
|
+
p.toolCallId
|
|
28115
|
+
);
|
|
27727
28116
|
socket.send(
|
|
27728
28117
|
JSON.stringify({
|
|
27729
28118
|
type: "tool_request",
|
|
@@ -27732,7 +28121,8 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27732
28121
|
tool: p.tool,
|
|
27733
28122
|
args: p.args,
|
|
27734
28123
|
requestPermission: p.requestPermission,
|
|
27735
|
-
risk: p.risk
|
|
28124
|
+
risk: p.risk,
|
|
28125
|
+
generation
|
|
27736
28126
|
})
|
|
27737
28127
|
);
|
|
27738
28128
|
}
|
|
@@ -27745,15 +28135,21 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27745
28135
|
* call resolved. Returns the side to continue, or null if there is no pending
|
|
27746
28136
|
* call (unknown id or already resolved — the delivery is idempotent).
|
|
27747
28137
|
*/
|
|
27748
|
-
async deliverClientToolResult(toolCallId, ok, result, error) {
|
|
28138
|
+
async deliverClientToolResult(toolCallId, ok, result, error, deliveredByClientId, deliveredGeneration) {
|
|
27749
28139
|
await this.ensureMigrated();
|
|
27750
28140
|
await this.ensurePendingClientTable();
|
|
27751
28141
|
const pendingCursor = await this.ctx.storage.sql.exec(
|
|
27752
|
-
`SELECT side, status
|
|
28142
|
+
`SELECT side, status, dispatched_client_id, dispatched_generation
|
|
28143
|
+
FROM pending_client_calls WHERE tool_call_id = ? LIMIT 1`,
|
|
27753
28144
|
toolCallId
|
|
27754
28145
|
);
|
|
27755
28146
|
const pending = pendingCursor.toArray()[0];
|
|
27756
28147
|
if (!pending || pending.status !== "pending") return null;
|
|
28148
|
+
if (pending.dispatched_client_id) {
|
|
28149
|
+
if (pending.dispatched_client_id !== deliveredByClientId || Number(pending.dispatched_generation ?? 0) !== (deliveredGeneration ?? -1)) {
|
|
28150
|
+
return null;
|
|
28151
|
+
}
|
|
28152
|
+
}
|
|
27757
28153
|
const side = pending.side === "b" ? "b" : "a";
|
|
27758
28154
|
const content = ok ? result ?? "" : `Error: ${error ?? "tool failed"}`;
|
|
27759
28155
|
const toolStatus = ok ? "success" : "error";
|
|
@@ -27989,7 +28385,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
27989
28385
|
parsed = null;
|
|
27990
28386
|
}
|
|
27991
28387
|
if (parsed && parsed.type === "tool_response") {
|
|
27992
|
-
this.resolveClientCall(parsed);
|
|
28388
|
+
this.resolveClientCall(ws, parsed);
|
|
27993
28389
|
return;
|
|
27994
28390
|
}
|
|
27995
28391
|
}
|
|
@@ -28011,6 +28407,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
28011
28407
|
} else if (attachment?.type === "bridge") {
|
|
28012
28408
|
this.bridgeSockets.delete(ws);
|
|
28013
28409
|
this.bridgeLastSeen.delete(ws);
|
|
28410
|
+
this.bridgeClientInfo.delete(ws);
|
|
28014
28411
|
this.failPendingClientCallsIfNoBridge();
|
|
28015
28412
|
}
|
|
28016
28413
|
}
|
|
@@ -28031,6 +28428,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
28031
28428
|
} else if (attachment?.type === "bridge") {
|
|
28032
28429
|
this.bridgeSockets.delete(ws);
|
|
28033
28430
|
this.bridgeLastSeen.delete(ws);
|
|
28431
|
+
this.bridgeClientInfo.delete(ws);
|
|
28034
28432
|
this.failPendingClientCallsIfNoBridge();
|
|
28035
28433
|
}
|
|
28036
28434
|
}
|
|
@@ -29591,7 +29989,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
|
|
|
29591
29989
|
};
|
|
29592
29990
|
|
|
29593
29991
|
// src/durable-objects/agentbuilder-migrations/0001_initial.ts
|
|
29594
|
-
var
|
|
29992
|
+
var migration35 = {
|
|
29595
29993
|
version: 1,
|
|
29596
29994
|
async up(sql) {
|
|
29597
29995
|
sql.exec(`
|
|
@@ -29690,7 +30088,7 @@ var migration34 = {
|
|
|
29690
30088
|
};
|
|
29691
30089
|
|
|
29692
30090
|
// src/durable-objects/agentbuilder-migrations/0002_add_tenvs_properties.ts
|
|
29693
|
-
var
|
|
30091
|
+
var migration36 = {
|
|
29694
30092
|
version: 2,
|
|
29695
30093
|
async up(sql) {
|
|
29696
30094
|
sql.exec(`ALTER TABLE threads ADD COLUMN tenvs TEXT`);
|
|
@@ -29702,7 +30100,7 @@ var migration35 = {
|
|
|
29702
30100
|
};
|
|
29703
30101
|
|
|
29704
30102
|
// src/durable-objects/agentbuilder-migrations/0003_add_parent_and_terminated.ts
|
|
29705
|
-
var
|
|
30103
|
+
var migration37 = {
|
|
29706
30104
|
version: 3,
|
|
29707
30105
|
async up(sql) {
|
|
29708
30106
|
sql.exec(`ALTER TABLE threads ADD COLUMN parent TEXT REFERENCES threads(id)`);
|
|
@@ -29715,7 +30113,7 @@ var migration36 = {
|
|
|
29715
30113
|
};
|
|
29716
30114
|
|
|
29717
30115
|
// src/durable-objects/agentbuilder-migrations/0004_add_env_tables.ts
|
|
29718
|
-
var
|
|
30116
|
+
var migration38 = {
|
|
29719
30117
|
version: 4,
|
|
29720
30118
|
async up(sql) {
|
|
29721
30119
|
sql.exec(`ALTER TABLE threads ADD COLUMN env TEXT`);
|
|
@@ -29739,7 +30137,7 @@ var migration37 = {
|
|
|
29739
30137
|
};
|
|
29740
30138
|
|
|
29741
30139
|
// src/durable-objects/agentbuilder-migrations/0005_add_pending_subagent_env_requests.ts
|
|
29742
|
-
var
|
|
30140
|
+
var migration39 = {
|
|
29743
30141
|
version: 5,
|
|
29744
30142
|
async up(sql) {
|
|
29745
30143
|
sql.exec(`
|
|
@@ -29765,7 +30163,7 @@ var migration38 = {
|
|
|
29765
30163
|
};
|
|
29766
30164
|
|
|
29767
30165
|
// src/durable-objects/agentbuilder-migrations/0006_add_env_value_types.ts
|
|
29768
|
-
var
|
|
30166
|
+
var migration40 = {
|
|
29769
30167
|
version: 6,
|
|
29770
30168
|
async up(sql) {
|
|
29771
30169
|
sql.exec(
|
|
@@ -29780,7 +30178,7 @@ var migration39 = {
|
|
|
29780
30178
|
};
|
|
29781
30179
|
|
|
29782
30180
|
// src/durable-objects/agentbuilder-migrations/0007_platform_identity_replica.ts
|
|
29783
|
-
var
|
|
30181
|
+
var migration41 = {
|
|
29784
30182
|
version: 7,
|
|
29785
30183
|
async up(sql) {
|
|
29786
30184
|
sql.exec(`ALTER TABLE users ADD COLUMN platform_user_id TEXT`);
|
|
@@ -29809,7 +30207,7 @@ var migration40 = {
|
|
|
29809
30207
|
};
|
|
29810
30208
|
|
|
29811
30209
|
// src/durable-objects/agentbuilder-migrations/0008_add_skills.ts
|
|
29812
|
-
var
|
|
30210
|
+
var migration42 = {
|
|
29813
30211
|
version: 8,
|
|
29814
30212
|
async up(sql) {
|
|
29815
30213
|
sql.exec(`
|
|
@@ -29840,7 +30238,7 @@ var migration41 = {
|
|
|
29840
30238
|
};
|
|
29841
30239
|
|
|
29842
30240
|
// src/durable-objects/agentbuilder-migrations/0009_add_user_permissions.ts
|
|
29843
|
-
var
|
|
30241
|
+
var migration43 = {
|
|
29844
30242
|
version: 9,
|
|
29845
30243
|
async up(sql) {
|
|
29846
30244
|
sql.exec(`ALTER TABLE users ADD COLUMN permissions TEXT`);
|
|
@@ -29851,7 +30249,7 @@ var migration42 = {
|
|
|
29851
30249
|
};
|
|
29852
30250
|
|
|
29853
30251
|
// src/durable-objects/agentbuilder-migrations/0010_add_device_logins.ts
|
|
29854
|
-
var
|
|
30252
|
+
var migration44 = {
|
|
29855
30253
|
version: 10,
|
|
29856
30254
|
async up(sql) {
|
|
29857
30255
|
sql.exec(`
|
|
@@ -29870,9 +30268,32 @@ var migration43 = {
|
|
|
29870
30268
|
}
|
|
29871
30269
|
};
|
|
29872
30270
|
|
|
30271
|
+
// src/durable-objects/agentbuilder-migrations/0011_add_user_values.ts
|
|
30272
|
+
var migration45 = {
|
|
30273
|
+
version: 11,
|
|
30274
|
+
async up(sql) {
|
|
30275
|
+
sql.exec(`
|
|
30276
|
+
CREATE TABLE IF NOT EXISTS user_values (
|
|
30277
|
+
user_id TEXT NOT NULL,
|
|
30278
|
+
key TEXT NOT NULL,
|
|
30279
|
+
value TEXT NOT NULL,
|
|
30280
|
+
created_at INTEGER NOT NULL,
|
|
30281
|
+
updated_at INTEGER NOT NULL,
|
|
30282
|
+
PRIMARY KEY (user_id, key)
|
|
30283
|
+
)
|
|
30284
|
+
`);
|
|
30285
|
+
sql.exec(`
|
|
30286
|
+
CREATE INDEX IF NOT EXISTS idx_user_values_user ON user_values(user_id)
|
|
30287
|
+
`);
|
|
30288
|
+
sql.exec(`
|
|
30289
|
+
INSERT OR REPLACE INTO _metadata (key, value) VALUES ('schema_version', '11')
|
|
30290
|
+
`);
|
|
30291
|
+
}
|
|
30292
|
+
};
|
|
30293
|
+
|
|
29873
30294
|
// src/durable-objects/agentbuilder-migrations/index.ts
|
|
29874
|
-
var migrations2 = [
|
|
29875
|
-
var LATEST_SCHEMA_VERSION2 =
|
|
30295
|
+
var migrations2 = [migration35, migration36, migration37, migration38, migration39, migration40, migration41, migration42, migration43, migration44, migration45];
|
|
30296
|
+
var LATEST_SCHEMA_VERSION2 = 11;
|
|
29876
30297
|
|
|
29877
30298
|
// src/utils/crypto.ts
|
|
29878
30299
|
var CryptoUtil = class {
|
|
@@ -30634,9 +31055,9 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
|
|
|
30634
31055
|
}
|
|
30635
31056
|
}
|
|
30636
31057
|
async runMigrations(fromVersion) {
|
|
30637
|
-
for (const
|
|
30638
|
-
if (
|
|
30639
|
-
await
|
|
31058
|
+
for (const migration46 of migrations2) {
|
|
31059
|
+
if (migration46.version > fromVersion) {
|
|
31060
|
+
await migration46.up(this.ctx.storage.sql);
|
|
30640
31061
|
}
|
|
30641
31062
|
}
|
|
30642
31063
|
}
|
|
@@ -31180,6 +31601,117 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
|
|
|
31180
31601
|
async patchUserEnv(userId, params) {
|
|
31181
31602
|
await this.patchScopedEnv("user", userId, params);
|
|
31182
31603
|
}
|
|
31604
|
+
// ============================================================
|
|
31605
|
+
// Per-user durable key-value store (account-wide, not thread-tied)
|
|
31606
|
+
//
|
|
31607
|
+
// The account-scoped sibling of the per-thread `thread_values` store:
|
|
31608
|
+
// JSON documents keyed by (user_id, key), for state a user carries across
|
|
31609
|
+
// every thread on the instance (e.g. a coding client's machine/daemon
|
|
31610
|
+
// registry). Mirrors thread-KV semantics — null/undefined deletes.
|
|
31611
|
+
// ============================================================
|
|
31612
|
+
assertValidUserValueKey(key) {
|
|
31613
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
31614
|
+
throw new Error("User value key must be a non-empty string");
|
|
31615
|
+
}
|
|
31616
|
+
if (key.length > 512) {
|
|
31617
|
+
throw new Error("User value key must be at most 512 characters");
|
|
31618
|
+
}
|
|
31619
|
+
}
|
|
31620
|
+
assertValidUserValueUserId(userId) {
|
|
31621
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
31622
|
+
throw new Error("User value user_id must be a non-empty string");
|
|
31623
|
+
}
|
|
31624
|
+
}
|
|
31625
|
+
/** Read one JSON value from a user's account-wide key-value store. */
|
|
31626
|
+
async getUserValue(userId, key) {
|
|
31627
|
+
await this.ensureMigrated();
|
|
31628
|
+
this.assertValidUserValueUserId(userId);
|
|
31629
|
+
this.assertValidUserValueKey(key);
|
|
31630
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
31631
|
+
`SELECT value FROM user_values WHERE user_id = ? AND key = ? LIMIT 1`,
|
|
31632
|
+
userId,
|
|
31633
|
+
key
|
|
31634
|
+
);
|
|
31635
|
+
const raw = cursor.toArray()[0]?.value;
|
|
31636
|
+
if (raw === void 0) return null;
|
|
31637
|
+
try {
|
|
31638
|
+
return JSON.parse(raw);
|
|
31639
|
+
} catch (error) {
|
|
31640
|
+
throw new Error(
|
|
31641
|
+
`Stored user value for key "${key}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
31642
|
+
);
|
|
31643
|
+
}
|
|
31644
|
+
}
|
|
31645
|
+
/** Write one JSON value to a user's account-wide store; null/undefined deletes. */
|
|
31646
|
+
async setUserValue(userId, key, value) {
|
|
31647
|
+
await this.ensureMigrated();
|
|
31648
|
+
this.assertValidUserValueUserId(userId);
|
|
31649
|
+
this.assertValidUserValueKey(key);
|
|
31650
|
+
if (value === null || value === void 0) {
|
|
31651
|
+
await this.ctx.storage.sql.exec(
|
|
31652
|
+
`DELETE FROM user_values WHERE user_id = ? AND key = ?`,
|
|
31653
|
+
userId,
|
|
31654
|
+
key
|
|
31655
|
+
);
|
|
31656
|
+
return;
|
|
31657
|
+
}
|
|
31658
|
+
let serialized;
|
|
31659
|
+
try {
|
|
31660
|
+
serialized = JSON.stringify(value);
|
|
31661
|
+
} catch (error) {
|
|
31662
|
+
throw new Error(
|
|
31663
|
+
`User value for key "${key}" is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`
|
|
31664
|
+
);
|
|
31665
|
+
}
|
|
31666
|
+
if (typeof serialized !== "string") {
|
|
31667
|
+
throw new Error(`User value for key "${key}" is not JSON-serializable`);
|
|
31668
|
+
}
|
|
31669
|
+
const now = Date.now();
|
|
31670
|
+
await this.ctx.storage.sql.exec(
|
|
31671
|
+
`INSERT INTO user_values (user_id, key, value, created_at, updated_at)
|
|
31672
|
+
VALUES (?1, ?2, ?3, ?4, ?4)
|
|
31673
|
+
ON CONFLICT(user_id, key) DO UPDATE SET
|
|
31674
|
+
value = excluded.value,
|
|
31675
|
+
updated_at = excluded.updated_at`,
|
|
31676
|
+
userId,
|
|
31677
|
+
key,
|
|
31678
|
+
serialized,
|
|
31679
|
+
now
|
|
31680
|
+
);
|
|
31681
|
+
}
|
|
31682
|
+
/** List a user's account-wide values, optionally filtered by key prefix. */
|
|
31683
|
+
async listUserValues(userId, options = {}) {
|
|
31684
|
+
await this.ensureMigrated();
|
|
31685
|
+
this.assertValidUserValueUserId(userId);
|
|
31686
|
+
const limit = Math.min(Math.max(Math.trunc(options.limit ?? 100), 1), 500);
|
|
31687
|
+
const offset = Math.max(Math.trunc(options.offset ?? 0), 0);
|
|
31688
|
+
const prefix = typeof options.prefix === "string" && options.prefix.length > 0 ? options.prefix : null;
|
|
31689
|
+
const escapedPrefix = prefix ? `${prefix.replace(/[\\%_]/g, (m) => `\\${m}`)}%` : null;
|
|
31690
|
+
const where = escapedPrefix ? `WHERE user_id = ? AND key LIKE ? ESCAPE '\\'` : `WHERE user_id = ?`;
|
|
31691
|
+
const whereArgs = escapedPrefix ? [userId, escapedPrefix] : [userId];
|
|
31692
|
+
const totalCursor = await this.ctx.storage.sql.exec(
|
|
31693
|
+
`SELECT COUNT(*) AS total FROM user_values ${where}`,
|
|
31694
|
+
...whereArgs
|
|
31695
|
+
);
|
|
31696
|
+
const total = Number(totalCursor.toArray()[0]?.total ?? 0);
|
|
31697
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
31698
|
+
`SELECT key, value, updated_at FROM user_values ${where}
|
|
31699
|
+
ORDER BY key ASC LIMIT ? OFFSET ?`,
|
|
31700
|
+
...whereArgs,
|
|
31701
|
+
limit,
|
|
31702
|
+
offset
|
|
31703
|
+
);
|
|
31704
|
+
const entries = cursor.toArray().map((row) => {
|
|
31705
|
+
let value = null;
|
|
31706
|
+
try {
|
|
31707
|
+
value = JSON.parse(row.value);
|
|
31708
|
+
} catch {
|
|
31709
|
+
value = null;
|
|
31710
|
+
}
|
|
31711
|
+
return { key: row.key, value, updated_at: Number(row.updated_at) };
|
|
31712
|
+
});
|
|
31713
|
+
return { entries, total, limit, offset };
|
|
31714
|
+
}
|
|
31183
31715
|
async getMissingRequiredScopedSubagentEnv(params) {
|
|
31184
31716
|
await this.ensureMigrated();
|
|
31185
31717
|
const requiredScoped = await this.getRequiredScopedVariableNamesForAgent(
|