@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/runtime.js
CHANGED
|
@@ -1088,6 +1088,25 @@ async function awaitBounded(work, budgetMs, label) {
|
|
|
1088
1088
|
if (timer) clearTimeout(timer);
|
|
1089
1089
|
}
|
|
1090
1090
|
}
|
|
1091
|
+
function chunkCarriesPayload(chunk) {
|
|
1092
|
+
switch (chunk.type) {
|
|
1093
|
+
case "content-delta":
|
|
1094
|
+
case "reasoning-delta":
|
|
1095
|
+
return typeof chunk.delta === "string" && chunk.delta.length > 0;
|
|
1096
|
+
case "tool-call-delta":
|
|
1097
|
+
return typeof chunk.argumentsDelta === "string" && chunk.argumentsDelta.length > 0;
|
|
1098
|
+
case "tool-call-start":
|
|
1099
|
+
case "tool-call-done":
|
|
1100
|
+
case "finish":
|
|
1101
|
+
case "image-done":
|
|
1102
|
+
case "web-search-done":
|
|
1103
|
+
case "provider-tool-done":
|
|
1104
|
+
case "error":
|
|
1105
|
+
return true;
|
|
1106
|
+
default:
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1091
1110
|
function modelSupportsImageInput(modelDef) {
|
|
1092
1111
|
const supportsImages = modelDef.capabilities?.supportsImages;
|
|
1093
1112
|
if (typeof supportsImages === "boolean") {
|
|
@@ -1899,7 +1918,7 @@ var init_LLMRequest = __esm({
|
|
|
1899
1918
|
if (next.done) break;
|
|
1900
1919
|
const chunk = next.value;
|
|
1901
1920
|
state.thread.instance.noteExecutionProgress?.();
|
|
1902
|
-
if (firstChunkAt === 0) {
|
|
1921
|
+
if (firstChunkAt === 0 && chunkCarriesPayload(chunk)) {
|
|
1903
1922
|
firstChunkAt = Date.now();
|
|
1904
1923
|
emitLogPatch({ time_to_first_token_ms: firstChunkAt - requestSentAt });
|
|
1905
1924
|
}
|
|
@@ -13157,6 +13176,38 @@ var init_ThreadStateImpl = __esm({
|
|
|
13157
13176
|
}
|
|
13158
13177
|
await this._threadInstance.setValue(key, value);
|
|
13159
13178
|
}
|
|
13179
|
+
// Account-wide sibling of the thread KV: values scoped to the thread's user,
|
|
13180
|
+
// shared across every thread that user owns (backed by the singleton
|
|
13181
|
+
// DurableAgentBuilder `user_values` table).
|
|
13182
|
+
async getUserValue(key) {
|
|
13183
|
+
const userId = this._metadata.user_id;
|
|
13184
|
+
if (!userId) return null;
|
|
13185
|
+
const stub = this._getAgentBuilderStub();
|
|
13186
|
+
if (typeof stub.getUserValue !== "function") {
|
|
13187
|
+
throw new Error("getUserValue is not supported by this runtime");
|
|
13188
|
+
}
|
|
13189
|
+
return await stub.getUserValue(userId, key);
|
|
13190
|
+
}
|
|
13191
|
+
async setUserValue(key, value) {
|
|
13192
|
+
const userId = this._metadata.user_id;
|
|
13193
|
+
if (!userId) {
|
|
13194
|
+
throw new Error("setUserValue requires a thread with an associated user");
|
|
13195
|
+
}
|
|
13196
|
+
const stub = this._getAgentBuilderStub();
|
|
13197
|
+
if (typeof stub.setUserValue !== "function") {
|
|
13198
|
+
throw new Error("setUserValue is not supported by this runtime");
|
|
13199
|
+
}
|
|
13200
|
+
await stub.setUserValue(userId, key, value);
|
|
13201
|
+
}
|
|
13202
|
+
async listUserValues(options = {}) {
|
|
13203
|
+
const userId = this._metadata.user_id;
|
|
13204
|
+
if (!userId) return { entries: [], total: 0 };
|
|
13205
|
+
const stub = this._getAgentBuilderStub();
|
|
13206
|
+
if (typeof stub.listUserValues !== "function") {
|
|
13207
|
+
throw new Error("listUserValues is not supported by this runtime");
|
|
13208
|
+
}
|
|
13209
|
+
return stub.listUserValues(userId, options);
|
|
13210
|
+
}
|
|
13160
13211
|
// ─────────────────────────────────────────────────────────────────────────
|
|
13161
13212
|
// Client-forwarded tools
|
|
13162
13213
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -13200,7 +13251,7 @@ var init_ThreadStateImpl = __esm({
|
|
|
13200
13251
|
const instance = this._threadInstance;
|
|
13201
13252
|
const toolCallId = this._flowState?.currentToolCall?.id;
|
|
13202
13253
|
const mustPark = request.requestPermission != null && await this.forwardedCallNeedsApproval(request);
|
|
13203
|
-
if (mustPark && toolCallId && this._executionState && typeof instance.dispatchClientTool === "function" && typeof instance.hasBridge === "function" && instance.hasBridge()) {
|
|
13254
|
+
if (mustPark && toolCallId && this._executionState && typeof instance.dispatchClientTool === "function" && typeof instance.hasBridge === "function" && await instance.hasBridge()) {
|
|
13204
13255
|
const side = this._flowState?.currentSide === "b" ? "b" : "a";
|
|
13205
13256
|
await instance.dispatchClientTool(request, toolCallId, side);
|
|
13206
13257
|
this._executionState.stop();
|
|
@@ -14944,8 +14995,38 @@ var migration33 = {
|
|
|
14944
14995
|
}
|
|
14945
14996
|
};
|
|
14946
14997
|
|
|
14998
|
+
// src/durable-objects/migrations/034_add_owner_fencing.ts
|
|
14999
|
+
var migration34 = {
|
|
15000
|
+
version: 34,
|
|
15001
|
+
name: "add_owner_fencing",
|
|
15002
|
+
async up(sql) {
|
|
15003
|
+
sql.exec(`
|
|
15004
|
+
CREATE TABLE IF NOT EXISTS pending_client_calls (
|
|
15005
|
+
tool_call_id TEXT PRIMARY KEY,
|
|
15006
|
+
tool TEXT NOT NULL,
|
|
15007
|
+
args TEXT,
|
|
15008
|
+
request_permission TEXT,
|
|
15009
|
+
risk INTEGER,
|
|
15010
|
+
side TEXT NOT NULL DEFAULT 'a',
|
|
15011
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
15012
|
+
created_at INTEGER NOT NULL
|
|
15013
|
+
)
|
|
15014
|
+
`);
|
|
15015
|
+
const tableInfo = sql.exec(`PRAGMA table_info(pending_client_calls)`).toArray();
|
|
15016
|
+
const hasColumn = (name) => tableInfo.some((col) => col.name === name);
|
|
15017
|
+
if (!hasColumn("dispatched_client_id")) {
|
|
15018
|
+
sql.exec(`ALTER TABLE pending_client_calls ADD COLUMN dispatched_client_id TEXT`);
|
|
15019
|
+
}
|
|
15020
|
+
if (!hasColumn("dispatched_generation")) {
|
|
15021
|
+
sql.exec(
|
|
15022
|
+
`ALTER TABLE pending_client_calls ADD COLUMN dispatched_generation INTEGER NOT NULL DEFAULT 0`
|
|
15023
|
+
);
|
|
15024
|
+
}
|
|
15025
|
+
}
|
|
15026
|
+
};
|
|
15027
|
+
|
|
14947
15028
|
// src/durable-objects/migrations/index.ts
|
|
14948
|
-
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];
|
|
15029
|
+
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];
|
|
14949
15030
|
var LATEST_SCHEMA_VERSION = migrations.length;
|
|
14950
15031
|
|
|
14951
15032
|
// src/durable-objects/DurableThread.ts
|
|
@@ -15745,6 +15826,61 @@ function isQualifiedName(name) {
|
|
|
15745
15826
|
|
|
15746
15827
|
// src/durable-objects/DurableThread.ts
|
|
15747
15828
|
init_sessionTools();
|
|
15829
|
+
|
|
15830
|
+
// src/durable-objects/bridge-owner.ts
|
|
15831
|
+
var EXECUTION_OWNER_KEY = "execution_owner";
|
|
15832
|
+
var CLAIM_MODES = /* @__PURE__ */ new Set([
|
|
15833
|
+
"none",
|
|
15834
|
+
"if_unowned",
|
|
15835
|
+
"if_stale",
|
|
15836
|
+
"takeover"
|
|
15837
|
+
]);
|
|
15838
|
+
function parseBridgeClientParams(url) {
|
|
15839
|
+
const readParam = (name) => {
|
|
15840
|
+
const raw = url.searchParams.get(name);
|
|
15841
|
+
if (raw == null) return null;
|
|
15842
|
+
const trimmed = raw.trim().slice(0, 256);
|
|
15843
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
15844
|
+
};
|
|
15845
|
+
const clientId = readParam("client_id");
|
|
15846
|
+
const info = {
|
|
15847
|
+
clientId,
|
|
15848
|
+
clientName: readParam("client_name"),
|
|
15849
|
+
clientKind: readParam("client_kind")
|
|
15850
|
+
};
|
|
15851
|
+
const rawClaim = readParam("claim");
|
|
15852
|
+
let claim;
|
|
15853
|
+
if (rawClaim && CLAIM_MODES.has(rawClaim)) {
|
|
15854
|
+
claim = rawClaim;
|
|
15855
|
+
} else {
|
|
15856
|
+
claim = clientId ? "if_unowned" : "none";
|
|
15857
|
+
}
|
|
15858
|
+
return { info, claim };
|
|
15859
|
+
}
|
|
15860
|
+
function resolveBridgeClaim(options) {
|
|
15861
|
+
const { clientId, claim, owner, ownerHasLiveSocket } = options;
|
|
15862
|
+
if (!clientId || claim === "none") return false;
|
|
15863
|
+
if (!owner) return true;
|
|
15864
|
+
if (owner.client_id === clientId) return true;
|
|
15865
|
+
if (claim === "takeover") return true;
|
|
15866
|
+
if (claim === "if_stale") return !ownerHasLiveSocket;
|
|
15867
|
+
return false;
|
|
15868
|
+
}
|
|
15869
|
+
function selectBridgeSocket(candidates, ownerClientId) {
|
|
15870
|
+
if (ownerClientId == null) {
|
|
15871
|
+
return candidates.length > 0 ? candidates[0].socket : null;
|
|
15872
|
+
}
|
|
15873
|
+
let legacyFallback = null;
|
|
15874
|
+
for (const candidate of candidates) {
|
|
15875
|
+
if (candidate.clientId === ownerClientId) return candidate.socket;
|
|
15876
|
+
if (candidate.clientId == null && legacyFallback == null) {
|
|
15877
|
+
legacyFallback = candidate.socket;
|
|
15878
|
+
}
|
|
15879
|
+
}
|
|
15880
|
+
return legacyFallback;
|
|
15881
|
+
}
|
|
15882
|
+
|
|
15883
|
+
// src/durable-objects/DurableThread.ts
|
|
15748
15884
|
init_code_execution();
|
|
15749
15885
|
var DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
15750
15886
|
var BRIDGE_LIVENESS_TIMEOUT_MS = 20 * 1e3;
|
|
@@ -15910,6 +16046,14 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
15910
16046
|
// Last time we heard anything (incl. heartbeat pings) from each bridge socket,
|
|
15911
16047
|
// used to detect a dead/half-open client quickly while a tool is in flight.
|
|
15912
16048
|
bridgeLastSeen = /* @__PURE__ */ new Map();
|
|
16049
|
+
// Identity each bridge socket presented at connect (`client_id` etc.). Legacy
|
|
16050
|
+
// clients present nothing and map to null fields. Restored from the socket
|
|
16051
|
+
// attachment after hibernation.
|
|
16052
|
+
bridgeClientInfo = /* @__PURE__ */ new Map();
|
|
16053
|
+
// In-memory mirror of the thread-KV `execution_owner` record. `undefined`
|
|
16054
|
+
// means "not loaded yet"; `null` means "loaded, no owner". Kept coherent by
|
|
16055
|
+
// setValue() so external KV writes (release/reassign) invalidate it too.
|
|
16056
|
+
executionOwnerCache = void 0;
|
|
15913
16057
|
pendingClientCalls = /* @__PURE__ */ new Map();
|
|
15914
16058
|
alarmQueue;
|
|
15915
16059
|
alarmQueueRearmed = false;
|
|
@@ -15980,6 +16124,11 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
15980
16124
|
} else if (attachment?.type === "bridge") {
|
|
15981
16125
|
this.bridgeSockets.add(ws);
|
|
15982
16126
|
this.bridgeLastSeen.set(ws, Date.now());
|
|
16127
|
+
this.bridgeClientInfo.set(ws, {
|
|
16128
|
+
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
16129
|
+
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
16130
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
16131
|
+
});
|
|
15983
16132
|
}
|
|
15984
16133
|
}
|
|
15985
16134
|
}
|
|
@@ -16473,6 +16622,9 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
16473
16622
|
async setValue(key, value) {
|
|
16474
16623
|
await this.ensureMigrated();
|
|
16475
16624
|
this.assertValidThreadValueKey(key);
|
|
16625
|
+
if (key === EXECUTION_OWNER_KEY) {
|
|
16626
|
+
this.executionOwnerCache = this.parseExecutionOwner(value);
|
|
16627
|
+
}
|
|
16476
16628
|
const updatedAt = Date.now() * 1e3;
|
|
16477
16629
|
if (value === null || value === void 0) {
|
|
16478
16630
|
await this.ctx.storage.sql.exec(`DELETE FROM thread_values WHERE key = ?`, key);
|
|
@@ -17120,9 +17272,9 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
17120
17272
|
* Each migration is run in order, starting from the current version + 1.
|
|
17121
17273
|
*/
|
|
17122
17274
|
async runMigrations(fromVersion) {
|
|
17123
|
-
for (const
|
|
17124
|
-
if (
|
|
17125
|
-
await
|
|
17275
|
+
for (const migration46 of migrations) {
|
|
17276
|
+
if (migration46.version > fromVersion) {
|
|
17277
|
+
await migration46.up(this.ctx.storage.sql);
|
|
17126
17278
|
}
|
|
17127
17279
|
}
|
|
17128
17280
|
}
|
|
@@ -19169,20 +19321,208 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19169
19321
|
async handleBridgeWebSocketUpgrade(request) {
|
|
19170
19322
|
const pair = new WebSocketPair();
|
|
19171
19323
|
const [client, server] = Object.values(pair);
|
|
19324
|
+
const requestUrl2 = new URL(request.url);
|
|
19325
|
+
const { info, claim } = parseBridgeClientParams(requestUrl2);
|
|
19326
|
+
const routeThreadId = requestUrl2.pathname.match(/\/api\/threads\/([^/]+)\//)?.[1] ?? null;
|
|
19172
19327
|
this.ctx.acceptWebSocket(server);
|
|
19173
|
-
server.serializeAttachment({
|
|
19328
|
+
server.serializeAttachment({
|
|
19329
|
+
type: "bridge",
|
|
19330
|
+
clientId: info.clientId,
|
|
19331
|
+
clientName: info.clientName,
|
|
19332
|
+
clientKind: info.clientKind
|
|
19333
|
+
});
|
|
19174
19334
|
this.bridgeSockets.add(server);
|
|
19175
19335
|
this.bridgeLastSeen.set(server, Date.now());
|
|
19336
|
+
this.bridgeClientInfo.set(server, info);
|
|
19337
|
+
if (info.clientId) {
|
|
19338
|
+
for (const ws of Array.from(this.bridgeSockets)) {
|
|
19339
|
+
if (ws === server) continue;
|
|
19340
|
+
if ((this.bridgeClientInfo.get(ws)?.clientId ?? null) !== info.clientId) continue;
|
|
19341
|
+
this.bridgeSockets.delete(ws);
|
|
19342
|
+
this.bridgeLastSeen.delete(ws);
|
|
19343
|
+
this.bridgeClientInfo.delete(ws);
|
|
19344
|
+
try {
|
|
19345
|
+
ws.send(JSON.stringify({ type: "superseded" }));
|
|
19346
|
+
} catch {
|
|
19347
|
+
}
|
|
19348
|
+
try {
|
|
19349
|
+
ws.close(4001, "Superseded by a newer connection from this client");
|
|
19350
|
+
} catch {
|
|
19351
|
+
}
|
|
19352
|
+
}
|
|
19353
|
+
}
|
|
19354
|
+
let owner = await this.getExecutionOwner();
|
|
19355
|
+
let claimRefused = null;
|
|
19356
|
+
const shouldClaim = resolveBridgeClaim({
|
|
19357
|
+
clientId: info.clientId,
|
|
19358
|
+
claim,
|
|
19359
|
+
owner,
|
|
19360
|
+
ownerHasLiveSocket: owner ? this.hasLiveBridgeSocketForClient(owner.client_id, server) : false
|
|
19361
|
+
});
|
|
19362
|
+
if (shouldClaim && info.clientId) {
|
|
19363
|
+
const changed = !owner || owner.client_id !== info.clientId;
|
|
19364
|
+
const ownerLive = owner ? this.hasLiveBridgeSocketForClient(owner.client_id, server) : false;
|
|
19365
|
+
if (changed && ownerLive && await this.hasInFlightClientCalls()) {
|
|
19366
|
+
claimRefused = "in_flight";
|
|
19367
|
+
} else {
|
|
19368
|
+
const previous = owner;
|
|
19369
|
+
const next = {
|
|
19370
|
+
client_id: info.clientId,
|
|
19371
|
+
client_name: info.clientName,
|
|
19372
|
+
client_kind: info.clientKind,
|
|
19373
|
+
claimed_at: Date.now(),
|
|
19374
|
+
generation: changed ? (owner?.generation ?? 0) + 1 : owner?.generation ?? 1
|
|
19375
|
+
};
|
|
19376
|
+
await this.setExecutionOwner(next);
|
|
19377
|
+
owner = next;
|
|
19378
|
+
if (changed) {
|
|
19379
|
+
this.broadcastOwnerChanged(next, server);
|
|
19380
|
+
if (previous) await this.failStalePendingWork(previous, routeThreadId);
|
|
19381
|
+
}
|
|
19382
|
+
}
|
|
19383
|
+
}
|
|
19384
|
+
const isOwner = owner ? info.clientId === owner.client_id : true;
|
|
19176
19385
|
try {
|
|
19177
|
-
server.send(
|
|
19386
|
+
server.send(
|
|
19387
|
+
JSON.stringify({
|
|
19388
|
+
type: "bridge_ready",
|
|
19389
|
+
threadId: this.ctx.id.toString(),
|
|
19390
|
+
owner: isOwner,
|
|
19391
|
+
...claimRefused ? { claim_refused: claimRefused } : {},
|
|
19392
|
+
current_owner: owner ? {
|
|
19393
|
+
client_id: owner.client_id,
|
|
19394
|
+
client_name: owner.client_name ?? null,
|
|
19395
|
+
client_kind: owner.client_kind ?? null,
|
|
19396
|
+
generation: owner.generation
|
|
19397
|
+
} : null
|
|
19398
|
+
})
|
|
19399
|
+
);
|
|
19178
19400
|
} catch {
|
|
19179
19401
|
}
|
|
19180
|
-
|
|
19402
|
+
if (isOwner) {
|
|
19403
|
+
void this.resendPendingClientCalls(server);
|
|
19404
|
+
}
|
|
19181
19405
|
return new Response(null, {
|
|
19182
19406
|
status: 101,
|
|
19183
19407
|
webSocket: client
|
|
19184
19408
|
});
|
|
19185
19409
|
}
|
|
19410
|
+
/** Parse a raw KV value into an {@link ExecutionOwner}, or null. */
|
|
19411
|
+
parseExecutionOwner(value) {
|
|
19412
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
19413
|
+
const record = value;
|
|
19414
|
+
if (typeof record.client_id !== "string" || record.client_id.length === 0) {
|
|
19415
|
+
return null;
|
|
19416
|
+
}
|
|
19417
|
+
return {
|
|
19418
|
+
client_id: record.client_id,
|
|
19419
|
+
client_name: typeof record.client_name === "string" ? record.client_name : null,
|
|
19420
|
+
client_kind: typeof record.client_kind === "string" ? record.client_kind : null,
|
|
19421
|
+
claimed_at: typeof record.claimed_at === "number" ? record.claimed_at : 0,
|
|
19422
|
+
generation: typeof record.generation === "number" ? record.generation : 0
|
|
19423
|
+
};
|
|
19424
|
+
}
|
|
19425
|
+
/** Load (and cache) the thread's persisted execution-owner record. */
|
|
19426
|
+
async getExecutionOwner() {
|
|
19427
|
+
if (this.executionOwnerCache !== void 0) return this.executionOwnerCache;
|
|
19428
|
+
const raw = await this.getValue(EXECUTION_OWNER_KEY);
|
|
19429
|
+
this.executionOwnerCache = this.parseExecutionOwner(raw);
|
|
19430
|
+
return this.executionOwnerCache;
|
|
19431
|
+
}
|
|
19432
|
+
/** Owner id from the in-memory mirror only (for sync paths); null when
|
|
19433
|
+
* unloaded or unowned — both degrade to the historical any-socket rule. */
|
|
19434
|
+
currentExecutionOwnerId() {
|
|
19435
|
+
return this.executionOwnerCache?.client_id ?? null;
|
|
19436
|
+
}
|
|
19437
|
+
/** Persist + cache the execution-owner record (null releases ownership). */
|
|
19438
|
+
async setExecutionOwner(owner) {
|
|
19439
|
+
await this.setValue(EXECUTION_OWNER_KEY, owner);
|
|
19440
|
+
}
|
|
19441
|
+
/** Whether the given client id has a live bridge socket (excluding `except`). */
|
|
19442
|
+
hasLiveBridgeSocketForClient(clientId, except) {
|
|
19443
|
+
const now = Date.now();
|
|
19444
|
+
for (const ws of this.bridgeSockets) {
|
|
19445
|
+
if (ws === except) continue;
|
|
19446
|
+
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
19447
|
+
const lastSeen = this.bridgeLastSeen.get(ws) ?? 0;
|
|
19448
|
+
if (now - lastSeen > BRIDGE_LIVENESS_TIMEOUT_MS) continue;
|
|
19449
|
+
if ((this.bridgeClientInfo.get(ws)?.clientId ?? null) === clientId) return true;
|
|
19450
|
+
}
|
|
19451
|
+
return false;
|
|
19452
|
+
}
|
|
19453
|
+
/** Tell every other bridge socket that execution ownership moved. */
|
|
19454
|
+
broadcastOwnerChanged(owner, except) {
|
|
19455
|
+
const frame = JSON.stringify({
|
|
19456
|
+
type: "owner_changed",
|
|
19457
|
+
owner: {
|
|
19458
|
+
client_id: owner.client_id,
|
|
19459
|
+
client_name: owner.client_name ?? null,
|
|
19460
|
+
client_kind: owner.client_kind ?? null,
|
|
19461
|
+
generation: owner.generation
|
|
19462
|
+
}
|
|
19463
|
+
});
|
|
19464
|
+
for (const ws of this.bridgeSockets) {
|
|
19465
|
+
if (ws === except) continue;
|
|
19466
|
+
try {
|
|
19467
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(frame);
|
|
19468
|
+
} catch {
|
|
19469
|
+
}
|
|
19470
|
+
}
|
|
19471
|
+
}
|
|
19472
|
+
/** Whether any forwarded call is mid-flight (blocking await or parked). */
|
|
19473
|
+
async hasInFlightClientCalls() {
|
|
19474
|
+
if (this.pendingClientCalls.size > 0) return true;
|
|
19475
|
+
await this.ensureMigrated();
|
|
19476
|
+
await this.ensurePendingClientTable();
|
|
19477
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
19478
|
+
`SELECT COUNT(*) AS total FROM pending_client_calls WHERE status = 'pending'`
|
|
19479
|
+
);
|
|
19480
|
+
return Number(cursor.toArray()[0]?.total ?? 0) > 0;
|
|
19481
|
+
}
|
|
19482
|
+
/**
|
|
19483
|
+
* Ownership moved away from a dead client: settle everything that was
|
|
19484
|
+
* dispatched to it. In-memory blocking calls fail immediately; parked
|
|
19485
|
+
* durable calls get an error result and the turn resumes — re-sending them
|
|
19486
|
+
* to the NEW owner would silently re-run the operation on a different
|
|
19487
|
+
* machine's filesystem, which is worse than an honest retryable error.
|
|
19488
|
+
*/
|
|
19489
|
+
async failStalePendingWork(previous, threadId) {
|
|
19490
|
+
for (const id of Array.from(this.pendingClientCalls.keys())) {
|
|
19491
|
+
this.settleClientCall(id, {
|
|
19492
|
+
ok: false,
|
|
19493
|
+
error: "The client executing this operation was replaced before it finished.",
|
|
19494
|
+
data: { noClient: true }
|
|
19495
|
+
});
|
|
19496
|
+
}
|
|
19497
|
+
try {
|
|
19498
|
+
await this.ensureMigrated();
|
|
19499
|
+
await this.ensurePendingClientTable();
|
|
19500
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
19501
|
+
`SELECT tool_call_id FROM pending_client_calls
|
|
19502
|
+
WHERE status = 'pending' AND dispatched_client_id = ?`,
|
|
19503
|
+
previous.client_id
|
|
19504
|
+
);
|
|
19505
|
+
const sides = /* @__PURE__ */ new Set();
|
|
19506
|
+
for (const row of cursor.toArray()) {
|
|
19507
|
+
const delivered = await this.deliverClientToolResult(
|
|
19508
|
+
row.tool_call_id,
|
|
19509
|
+
false,
|
|
19510
|
+
void 0,
|
|
19511
|
+
"The client executing this operation was replaced before it finished. Retry if the work is still needed.",
|
|
19512
|
+
previous.client_id,
|
|
19513
|
+
previous.generation
|
|
19514
|
+
);
|
|
19515
|
+
if (delivered) sides.add(delivered.side);
|
|
19516
|
+
}
|
|
19517
|
+
if (threadId) {
|
|
19518
|
+
for (const side of sides) {
|
|
19519
|
+
await this.continueExecution(threadId, side);
|
|
19520
|
+
}
|
|
19521
|
+
}
|
|
19522
|
+
} catch (error) {
|
|
19523
|
+
console.error("[DurableThread] failStalePendingWork:", error);
|
|
19524
|
+
}
|
|
19525
|
+
}
|
|
19186
19526
|
/**
|
|
19187
19527
|
* Forward a tool operation to a connected bridge client and await its result.
|
|
19188
19528
|
*
|
|
@@ -19191,7 +19531,8 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19191
19531
|
* denial) for the model to reason about.
|
|
19192
19532
|
*/
|
|
19193
19533
|
async callClientTool(request) {
|
|
19194
|
-
const
|
|
19534
|
+
const owner = await this.getExecutionOwner();
|
|
19535
|
+
const socket = this.getOpenBridgeSocket(owner?.client_id ?? null);
|
|
19195
19536
|
if (!socket) {
|
|
19196
19537
|
return {
|
|
19197
19538
|
ok: false,
|
|
@@ -19222,7 +19563,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19222
19563
|
});
|
|
19223
19564
|
}
|
|
19224
19565
|
}, 5e3);
|
|
19225
|
-
this.pendingClientCalls.set(id, { resolve, timer, liveness });
|
|
19566
|
+
this.pendingClientCalls.set(id, { resolve, timer, liveness, socket });
|
|
19226
19567
|
const frame = JSON.stringify({
|
|
19227
19568
|
type: "tool_request",
|
|
19228
19569
|
id,
|
|
@@ -19256,6 +19597,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19256
19597
|
markBridgeDead(socket) {
|
|
19257
19598
|
this.bridgeSockets.delete(socket);
|
|
19258
19599
|
this.bridgeLastSeen.delete(socket);
|
|
19600
|
+
this.bridgeClientInfo.delete(socket);
|
|
19259
19601
|
try {
|
|
19260
19602
|
socket.close();
|
|
19261
19603
|
} catch {
|
|
@@ -19263,33 +19605,42 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19263
19605
|
this.failPendingClientCallsIfNoBridge();
|
|
19264
19606
|
}
|
|
19265
19607
|
/**
|
|
19266
|
-
* Return the
|
|
19267
|
-
*
|
|
19608
|
+
* Return the bridge socket a forwarded call should be dispatched to, pruning
|
|
19609
|
+
* dead ones. A half-open client (idle NAT/proxy drop) keeps a
|
|
19268
19610
|
* `readyState === OPEN` socket that never fires close/error, so we also treat
|
|
19269
19611
|
* a socket we haven't heard from within {@link BRIDGE_LIVENESS_TIMEOUT_MS} as
|
|
19270
19612
|
* dead. The CLI heartbeats every few seconds, so this only trips on a truly
|
|
19271
19613
|
* gone client — and it means we won't dispatch a forwarded call into the void.
|
|
19614
|
+
*
|
|
19615
|
+
* With an execution owner recorded, only the owner's socket (or, as a
|
|
19616
|
+
* back-compat fallback, an unidentified legacy socket) is eligible; with no
|
|
19617
|
+
* owner, the first live socket wins as it always has.
|
|
19272
19618
|
*/
|
|
19273
|
-
getOpenBridgeSocket() {
|
|
19619
|
+
getOpenBridgeSocket(ownerClientId) {
|
|
19274
19620
|
const now = Date.now();
|
|
19621
|
+
const live = [];
|
|
19275
19622
|
for (const ws of Array.from(this.bridgeSockets)) {
|
|
19276
19623
|
const lastSeen = this.bridgeLastSeen.get(ws) ?? 0;
|
|
19277
19624
|
if (ws.readyState === WebSocket.OPEN && now - lastSeen <= BRIDGE_LIVENESS_TIMEOUT_MS) {
|
|
19278
|
-
|
|
19625
|
+
live.push({ socket: ws, clientId: this.bridgeClientInfo.get(ws)?.clientId ?? null });
|
|
19626
|
+
continue;
|
|
19279
19627
|
}
|
|
19280
19628
|
this.bridgeSockets.delete(ws);
|
|
19281
19629
|
this.bridgeLastSeen.delete(ws);
|
|
19630
|
+
this.bridgeClientInfo.delete(ws);
|
|
19282
19631
|
try {
|
|
19283
19632
|
ws.close();
|
|
19284
19633
|
} catch {
|
|
19285
19634
|
}
|
|
19286
19635
|
}
|
|
19287
|
-
return
|
|
19636
|
+
return selectBridgeSocket(live, ownerClientId);
|
|
19288
19637
|
}
|
|
19289
19638
|
/** Resolve an in-flight forwarded tool call from a client `tool_response`. */
|
|
19290
|
-
resolveClientCall(payload) {
|
|
19639
|
+
resolveClientCall(ws, payload) {
|
|
19291
19640
|
const id = typeof payload.id === "string" ? payload.id : null;
|
|
19292
19641
|
if (!id) return;
|
|
19642
|
+
const pending = this.pendingClientCalls.get(id);
|
|
19643
|
+
if (pending?.socket && pending.socket !== ws) return;
|
|
19293
19644
|
this.settleClientCall(id, {
|
|
19294
19645
|
ok: payload.ok === true,
|
|
19295
19646
|
result: typeof payload.result === "string" ? payload.result : void 0,
|
|
@@ -19298,11 +19649,14 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19298
19649
|
});
|
|
19299
19650
|
}
|
|
19300
19651
|
/**
|
|
19301
|
-
* When the last bridge client
|
|
19302
|
-
*
|
|
19652
|
+
* When the last bridge client that could serve forwarded calls disconnects,
|
|
19653
|
+
* fail any in-flight ones so the agent loop doesn't hang waiting on a client
|
|
19654
|
+
* that is gone. Uses the in-memory owner mirror (sync path); when the mirror
|
|
19655
|
+
* isn't loaded this degrades to the historical any-socket check, and the
|
|
19656
|
+
* per-call liveness interval still catches a dead dispatch target.
|
|
19303
19657
|
*/
|
|
19304
19658
|
failPendingClientCallsIfNoBridge() {
|
|
19305
|
-
if (this.getOpenBridgeSocket()) return;
|
|
19659
|
+
if (this.getOpenBridgeSocket(this.currentExecutionOwnerId())) return;
|
|
19306
19660
|
for (const id of Array.from(this.pendingClientCalls.keys())) {
|
|
19307
19661
|
this.settleClientCall(id, {
|
|
19308
19662
|
ok: false,
|
|
@@ -19311,9 +19665,11 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19311
19665
|
});
|
|
19312
19666
|
}
|
|
19313
19667
|
}
|
|
19314
|
-
/** Whether a bridge client
|
|
19315
|
-
|
|
19316
|
-
|
|
19668
|
+
/** Whether a bridge client that can execute forwarded calls is connected
|
|
19669
|
+
* right now (used to decide durable park). */
|
|
19670
|
+
async hasBridge() {
|
|
19671
|
+
const owner = await this.getExecutionOwner();
|
|
19672
|
+
return this.getOpenBridgeSocket(owner?.client_id ?? null) !== null;
|
|
19317
19673
|
}
|
|
19318
19674
|
// ─── Durable forwarded tool calls (park & resume) ──────────────────────────
|
|
19319
19675
|
// A permission-gated forwarded call must not hold the agent loop open across
|
|
@@ -19330,7 +19686,9 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19330
19686
|
risk INTEGER,
|
|
19331
19687
|
side TEXT NOT NULL DEFAULT 'a',
|
|
19332
19688
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
19333
|
-
created_at INTEGER NOT NULL
|
|
19689
|
+
created_at INTEGER NOT NULL,
|
|
19690
|
+
dispatched_client_id TEXT,
|
|
19691
|
+
dispatched_generation INTEGER NOT NULL DEFAULT 0
|
|
19334
19692
|
)
|
|
19335
19693
|
`);
|
|
19336
19694
|
}
|
|
@@ -19355,13 +19713,31 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19355
19713
|
side,
|
|
19356
19714
|
Date.now()
|
|
19357
19715
|
);
|
|
19358
|
-
return { dispatched: this.sendClientToolRequest(toolCallId, request) };
|
|
19716
|
+
return { dispatched: await this.sendClientToolRequest(toolCallId, request) };
|
|
19359
19717
|
}
|
|
19360
|
-
/**
|
|
19361
|
-
|
|
19362
|
-
|
|
19718
|
+
/**
|
|
19719
|
+
* Send one durable `tool_request` frame to the bridge if a client is
|
|
19720
|
+
* connected — and stamp the pending row with the identity+generation it was
|
|
19721
|
+
* dispatched to, fencing the eventual HTTP delivery to that client. A
|
|
19722
|
+
* legacy (unidentified) executor leaves the row unstamped, keeping the
|
|
19723
|
+
* historical accept-anything delivery for old CLIs.
|
|
19724
|
+
*/
|
|
19725
|
+
async sendClientToolRequest(toolCallId, request) {
|
|
19726
|
+
const owner = await this.getExecutionOwner();
|
|
19727
|
+
const socket = this.getOpenBridgeSocket(owner?.client_id ?? null);
|
|
19363
19728
|
if (!socket) return false;
|
|
19729
|
+
const socketClientId = this.bridgeClientInfo.get(socket)?.clientId ?? null;
|
|
19730
|
+
const fenced = owner && socketClientId === owner.client_id;
|
|
19731
|
+
const generation = fenced ? owner.generation : 0;
|
|
19364
19732
|
try {
|
|
19733
|
+
await this.ctx.storage.sql.exec(
|
|
19734
|
+
`UPDATE pending_client_calls
|
|
19735
|
+
SET dispatched_client_id = ?, dispatched_generation = ?
|
|
19736
|
+
WHERE tool_call_id = ? AND status = 'pending'`,
|
|
19737
|
+
fenced ? owner.client_id : null,
|
|
19738
|
+
generation,
|
|
19739
|
+
toolCallId
|
|
19740
|
+
);
|
|
19365
19741
|
socket.send(
|
|
19366
19742
|
JSON.stringify({
|
|
19367
19743
|
type: "tool_request",
|
|
@@ -19370,7 +19746,8 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19370
19746
|
tool: request.tool,
|
|
19371
19747
|
args: request.args ?? {},
|
|
19372
19748
|
requestPermission: request.requestPermission ?? null,
|
|
19373
|
-
risk: typeof request.risk === "number" ? request.risk : null
|
|
19749
|
+
risk: typeof request.risk === "number" ? request.risk : null,
|
|
19750
|
+
generation
|
|
19374
19751
|
})
|
|
19375
19752
|
);
|
|
19376
19753
|
this.bridgeLastSeen.set(socket, Date.now());
|
|
@@ -19406,9 +19783,21 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19406
19783
|
/** Re-send all pending durable requests to a freshly connected bridge socket. */
|
|
19407
19784
|
async resendPendingClientCalls(socket) {
|
|
19408
19785
|
try {
|
|
19786
|
+
const owner = await this.getExecutionOwner();
|
|
19787
|
+
const socketClientId = this.bridgeClientInfo.get(socket)?.clientId ?? null;
|
|
19788
|
+
const fenced = owner && socketClientId === owner.client_id;
|
|
19789
|
+
const generation = fenced ? owner.generation : 0;
|
|
19409
19790
|
const pending = await this.getPendingClientCalls();
|
|
19410
19791
|
for (const p of pending) {
|
|
19411
19792
|
if (socket.readyState !== WebSocket.OPEN) break;
|
|
19793
|
+
await this.ctx.storage.sql.exec(
|
|
19794
|
+
`UPDATE pending_client_calls
|
|
19795
|
+
SET dispatched_client_id = ?, dispatched_generation = ?
|
|
19796
|
+
WHERE tool_call_id = ? AND status = 'pending'`,
|
|
19797
|
+
fenced ? owner.client_id : null,
|
|
19798
|
+
generation,
|
|
19799
|
+
p.toolCallId
|
|
19800
|
+
);
|
|
19412
19801
|
socket.send(
|
|
19413
19802
|
JSON.stringify({
|
|
19414
19803
|
type: "tool_request",
|
|
@@ -19417,7 +19806,8 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19417
19806
|
tool: p.tool,
|
|
19418
19807
|
args: p.args,
|
|
19419
19808
|
requestPermission: p.requestPermission,
|
|
19420
|
-
risk: p.risk
|
|
19809
|
+
risk: p.risk,
|
|
19810
|
+
generation
|
|
19421
19811
|
})
|
|
19422
19812
|
);
|
|
19423
19813
|
}
|
|
@@ -19430,15 +19820,21 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19430
19820
|
* call resolved. Returns the side to continue, or null if there is no pending
|
|
19431
19821
|
* call (unknown id or already resolved — the delivery is idempotent).
|
|
19432
19822
|
*/
|
|
19433
|
-
async deliverClientToolResult(toolCallId, ok, result, error) {
|
|
19823
|
+
async deliverClientToolResult(toolCallId, ok, result, error, deliveredByClientId, deliveredGeneration) {
|
|
19434
19824
|
await this.ensureMigrated();
|
|
19435
19825
|
await this.ensurePendingClientTable();
|
|
19436
19826
|
const pendingCursor = await this.ctx.storage.sql.exec(
|
|
19437
|
-
`SELECT side, status
|
|
19827
|
+
`SELECT side, status, dispatched_client_id, dispatched_generation
|
|
19828
|
+
FROM pending_client_calls WHERE tool_call_id = ? LIMIT 1`,
|
|
19438
19829
|
toolCallId
|
|
19439
19830
|
);
|
|
19440
19831
|
const pending = pendingCursor.toArray()[0];
|
|
19441
19832
|
if (!pending || pending.status !== "pending") return null;
|
|
19833
|
+
if (pending.dispatched_client_id) {
|
|
19834
|
+
if (pending.dispatched_client_id !== deliveredByClientId || Number(pending.dispatched_generation ?? 0) !== (deliveredGeneration ?? -1)) {
|
|
19835
|
+
return null;
|
|
19836
|
+
}
|
|
19837
|
+
}
|
|
19442
19838
|
const side = pending.side === "b" ? "b" : "a";
|
|
19443
19839
|
const content = ok ? result ?? "" : `Error: ${error ?? "tool failed"}`;
|
|
19444
19840
|
const toolStatus = ok ? "success" : "error";
|
|
@@ -19674,7 +20070,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19674
20070
|
parsed = null;
|
|
19675
20071
|
}
|
|
19676
20072
|
if (parsed && parsed.type === "tool_response") {
|
|
19677
|
-
this.resolveClientCall(parsed);
|
|
20073
|
+
this.resolveClientCall(ws, parsed);
|
|
19678
20074
|
return;
|
|
19679
20075
|
}
|
|
19680
20076
|
}
|
|
@@ -19696,6 +20092,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19696
20092
|
} else if (attachment?.type === "bridge") {
|
|
19697
20093
|
this.bridgeSockets.delete(ws);
|
|
19698
20094
|
this.bridgeLastSeen.delete(ws);
|
|
20095
|
+
this.bridgeClientInfo.delete(ws);
|
|
19699
20096
|
this.failPendingClientCallsIfNoBridge();
|
|
19700
20097
|
}
|
|
19701
20098
|
}
|
|
@@ -19716,6 +20113,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19716
20113
|
} else if (attachment?.type === "bridge") {
|
|
19717
20114
|
this.bridgeSockets.delete(ws);
|
|
19718
20115
|
this.bridgeLastSeen.delete(ws);
|
|
20116
|
+
this.bridgeClientInfo.delete(ws);
|
|
19719
20117
|
this.failPendingClientCallsIfNoBridge();
|
|
19720
20118
|
}
|
|
19721
20119
|
}
|
|
@@ -21276,7 +21674,7 @@ Its execution was lost (the executor restarted or hung) and automatic recovery w
|
|
|
21276
21674
|
};
|
|
21277
21675
|
|
|
21278
21676
|
// src/durable-objects/agentbuilder-migrations/0001_initial.ts
|
|
21279
|
-
var
|
|
21677
|
+
var migration35 = {
|
|
21280
21678
|
version: 1,
|
|
21281
21679
|
async up(sql) {
|
|
21282
21680
|
sql.exec(`
|
|
@@ -21375,7 +21773,7 @@ var migration34 = {
|
|
|
21375
21773
|
};
|
|
21376
21774
|
|
|
21377
21775
|
// src/durable-objects/agentbuilder-migrations/0002_add_tenvs_properties.ts
|
|
21378
|
-
var
|
|
21776
|
+
var migration36 = {
|
|
21379
21777
|
version: 2,
|
|
21380
21778
|
async up(sql) {
|
|
21381
21779
|
sql.exec(`ALTER TABLE threads ADD COLUMN tenvs TEXT`);
|
|
@@ -21387,7 +21785,7 @@ var migration35 = {
|
|
|
21387
21785
|
};
|
|
21388
21786
|
|
|
21389
21787
|
// src/durable-objects/agentbuilder-migrations/0003_add_parent_and_terminated.ts
|
|
21390
|
-
var
|
|
21788
|
+
var migration37 = {
|
|
21391
21789
|
version: 3,
|
|
21392
21790
|
async up(sql) {
|
|
21393
21791
|
sql.exec(`ALTER TABLE threads ADD COLUMN parent TEXT REFERENCES threads(id)`);
|
|
@@ -21400,7 +21798,7 @@ var migration36 = {
|
|
|
21400
21798
|
};
|
|
21401
21799
|
|
|
21402
21800
|
// src/durable-objects/agentbuilder-migrations/0004_add_env_tables.ts
|
|
21403
|
-
var
|
|
21801
|
+
var migration38 = {
|
|
21404
21802
|
version: 4,
|
|
21405
21803
|
async up(sql) {
|
|
21406
21804
|
sql.exec(`ALTER TABLE threads ADD COLUMN env TEXT`);
|
|
@@ -21424,7 +21822,7 @@ var migration37 = {
|
|
|
21424
21822
|
};
|
|
21425
21823
|
|
|
21426
21824
|
// src/durable-objects/agentbuilder-migrations/0005_add_pending_subagent_env_requests.ts
|
|
21427
|
-
var
|
|
21825
|
+
var migration39 = {
|
|
21428
21826
|
version: 5,
|
|
21429
21827
|
async up(sql) {
|
|
21430
21828
|
sql.exec(`
|
|
@@ -21450,7 +21848,7 @@ var migration38 = {
|
|
|
21450
21848
|
};
|
|
21451
21849
|
|
|
21452
21850
|
// src/durable-objects/agentbuilder-migrations/0006_add_env_value_types.ts
|
|
21453
|
-
var
|
|
21851
|
+
var migration40 = {
|
|
21454
21852
|
version: 6,
|
|
21455
21853
|
async up(sql) {
|
|
21456
21854
|
sql.exec(
|
|
@@ -21465,7 +21863,7 @@ var migration39 = {
|
|
|
21465
21863
|
};
|
|
21466
21864
|
|
|
21467
21865
|
// src/durable-objects/agentbuilder-migrations/0007_platform_identity_replica.ts
|
|
21468
|
-
var
|
|
21866
|
+
var migration41 = {
|
|
21469
21867
|
version: 7,
|
|
21470
21868
|
async up(sql) {
|
|
21471
21869
|
sql.exec(`ALTER TABLE users ADD COLUMN platform_user_id TEXT`);
|
|
@@ -21494,7 +21892,7 @@ var migration40 = {
|
|
|
21494
21892
|
};
|
|
21495
21893
|
|
|
21496
21894
|
// src/durable-objects/agentbuilder-migrations/0008_add_skills.ts
|
|
21497
|
-
var
|
|
21895
|
+
var migration42 = {
|
|
21498
21896
|
version: 8,
|
|
21499
21897
|
async up(sql) {
|
|
21500
21898
|
sql.exec(`
|
|
@@ -21525,7 +21923,7 @@ var migration41 = {
|
|
|
21525
21923
|
};
|
|
21526
21924
|
|
|
21527
21925
|
// src/durable-objects/agentbuilder-migrations/0009_add_user_permissions.ts
|
|
21528
|
-
var
|
|
21926
|
+
var migration43 = {
|
|
21529
21927
|
version: 9,
|
|
21530
21928
|
async up(sql) {
|
|
21531
21929
|
sql.exec(`ALTER TABLE users ADD COLUMN permissions TEXT`);
|
|
@@ -21536,7 +21934,7 @@ var migration42 = {
|
|
|
21536
21934
|
};
|
|
21537
21935
|
|
|
21538
21936
|
// src/durable-objects/agentbuilder-migrations/0010_add_device_logins.ts
|
|
21539
|
-
var
|
|
21937
|
+
var migration44 = {
|
|
21540
21938
|
version: 10,
|
|
21541
21939
|
async up(sql) {
|
|
21542
21940
|
sql.exec(`
|
|
@@ -21555,9 +21953,32 @@ var migration43 = {
|
|
|
21555
21953
|
}
|
|
21556
21954
|
};
|
|
21557
21955
|
|
|
21956
|
+
// src/durable-objects/agentbuilder-migrations/0011_add_user_values.ts
|
|
21957
|
+
var migration45 = {
|
|
21958
|
+
version: 11,
|
|
21959
|
+
async up(sql) {
|
|
21960
|
+
sql.exec(`
|
|
21961
|
+
CREATE TABLE IF NOT EXISTS user_values (
|
|
21962
|
+
user_id TEXT NOT NULL,
|
|
21963
|
+
key TEXT NOT NULL,
|
|
21964
|
+
value TEXT NOT NULL,
|
|
21965
|
+
created_at INTEGER NOT NULL,
|
|
21966
|
+
updated_at INTEGER NOT NULL,
|
|
21967
|
+
PRIMARY KEY (user_id, key)
|
|
21968
|
+
)
|
|
21969
|
+
`);
|
|
21970
|
+
sql.exec(`
|
|
21971
|
+
CREATE INDEX IF NOT EXISTS idx_user_values_user ON user_values(user_id)
|
|
21972
|
+
`);
|
|
21973
|
+
sql.exec(`
|
|
21974
|
+
INSERT OR REPLACE INTO _metadata (key, value) VALUES ('schema_version', '11')
|
|
21975
|
+
`);
|
|
21976
|
+
}
|
|
21977
|
+
};
|
|
21978
|
+
|
|
21558
21979
|
// src/durable-objects/agentbuilder-migrations/index.ts
|
|
21559
|
-
var migrations2 = [
|
|
21560
|
-
var LATEST_SCHEMA_VERSION2 =
|
|
21980
|
+
var migrations2 = [migration35, migration36, migration37, migration38, migration39, migration40, migration41, migration42, migration43, migration44, migration45];
|
|
21981
|
+
var LATEST_SCHEMA_VERSION2 = 11;
|
|
21561
21982
|
|
|
21562
21983
|
// src/utils/crypto.ts
|
|
21563
21984
|
var CryptoUtil = class {
|
|
@@ -22319,9 +22740,9 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
|
|
|
22319
22740
|
}
|
|
22320
22741
|
}
|
|
22321
22742
|
async runMigrations(fromVersion) {
|
|
22322
|
-
for (const
|
|
22323
|
-
if (
|
|
22324
|
-
await
|
|
22743
|
+
for (const migration46 of migrations2) {
|
|
22744
|
+
if (migration46.version > fromVersion) {
|
|
22745
|
+
await migration46.up(this.ctx.storage.sql);
|
|
22325
22746
|
}
|
|
22326
22747
|
}
|
|
22327
22748
|
}
|
|
@@ -22865,6 +23286,117 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
|
|
|
22865
23286
|
async patchUserEnv(userId, params) {
|
|
22866
23287
|
await this.patchScopedEnv("user", userId, params);
|
|
22867
23288
|
}
|
|
23289
|
+
// ============================================================
|
|
23290
|
+
// Per-user durable key-value store (account-wide, not thread-tied)
|
|
23291
|
+
//
|
|
23292
|
+
// The account-scoped sibling of the per-thread `thread_values` store:
|
|
23293
|
+
// JSON documents keyed by (user_id, key), for state a user carries across
|
|
23294
|
+
// every thread on the instance (e.g. a coding client's machine/daemon
|
|
23295
|
+
// registry). Mirrors thread-KV semantics — null/undefined deletes.
|
|
23296
|
+
// ============================================================
|
|
23297
|
+
assertValidUserValueKey(key) {
|
|
23298
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
23299
|
+
throw new Error("User value key must be a non-empty string");
|
|
23300
|
+
}
|
|
23301
|
+
if (key.length > 512) {
|
|
23302
|
+
throw new Error("User value key must be at most 512 characters");
|
|
23303
|
+
}
|
|
23304
|
+
}
|
|
23305
|
+
assertValidUserValueUserId(userId) {
|
|
23306
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
23307
|
+
throw new Error("User value user_id must be a non-empty string");
|
|
23308
|
+
}
|
|
23309
|
+
}
|
|
23310
|
+
/** Read one JSON value from a user's account-wide key-value store. */
|
|
23311
|
+
async getUserValue(userId, key) {
|
|
23312
|
+
await this.ensureMigrated();
|
|
23313
|
+
this.assertValidUserValueUserId(userId);
|
|
23314
|
+
this.assertValidUserValueKey(key);
|
|
23315
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
23316
|
+
`SELECT value FROM user_values WHERE user_id = ? AND key = ? LIMIT 1`,
|
|
23317
|
+
userId,
|
|
23318
|
+
key
|
|
23319
|
+
);
|
|
23320
|
+
const raw = cursor.toArray()[0]?.value;
|
|
23321
|
+
if (raw === void 0) return null;
|
|
23322
|
+
try {
|
|
23323
|
+
return JSON.parse(raw);
|
|
23324
|
+
} catch (error) {
|
|
23325
|
+
throw new Error(
|
|
23326
|
+
`Stored user value for key "${key}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
23327
|
+
);
|
|
23328
|
+
}
|
|
23329
|
+
}
|
|
23330
|
+
/** Write one JSON value to a user's account-wide store; null/undefined deletes. */
|
|
23331
|
+
async setUserValue(userId, key, value) {
|
|
23332
|
+
await this.ensureMigrated();
|
|
23333
|
+
this.assertValidUserValueUserId(userId);
|
|
23334
|
+
this.assertValidUserValueKey(key);
|
|
23335
|
+
if (value === null || value === void 0) {
|
|
23336
|
+
await this.ctx.storage.sql.exec(
|
|
23337
|
+
`DELETE FROM user_values WHERE user_id = ? AND key = ?`,
|
|
23338
|
+
userId,
|
|
23339
|
+
key
|
|
23340
|
+
);
|
|
23341
|
+
return;
|
|
23342
|
+
}
|
|
23343
|
+
let serialized;
|
|
23344
|
+
try {
|
|
23345
|
+
serialized = JSON.stringify(value);
|
|
23346
|
+
} catch (error) {
|
|
23347
|
+
throw new Error(
|
|
23348
|
+
`User value for key "${key}" is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`
|
|
23349
|
+
);
|
|
23350
|
+
}
|
|
23351
|
+
if (typeof serialized !== "string") {
|
|
23352
|
+
throw new Error(`User value for key "${key}" is not JSON-serializable`);
|
|
23353
|
+
}
|
|
23354
|
+
const now = Date.now();
|
|
23355
|
+
await this.ctx.storage.sql.exec(
|
|
23356
|
+
`INSERT INTO user_values (user_id, key, value, created_at, updated_at)
|
|
23357
|
+
VALUES (?1, ?2, ?3, ?4, ?4)
|
|
23358
|
+
ON CONFLICT(user_id, key) DO UPDATE SET
|
|
23359
|
+
value = excluded.value,
|
|
23360
|
+
updated_at = excluded.updated_at`,
|
|
23361
|
+
userId,
|
|
23362
|
+
key,
|
|
23363
|
+
serialized,
|
|
23364
|
+
now
|
|
23365
|
+
);
|
|
23366
|
+
}
|
|
23367
|
+
/** List a user's account-wide values, optionally filtered by key prefix. */
|
|
23368
|
+
async listUserValues(userId, options = {}) {
|
|
23369
|
+
await this.ensureMigrated();
|
|
23370
|
+
this.assertValidUserValueUserId(userId);
|
|
23371
|
+
const limit = Math.min(Math.max(Math.trunc(options.limit ?? 100), 1), 500);
|
|
23372
|
+
const offset = Math.max(Math.trunc(options.offset ?? 0), 0);
|
|
23373
|
+
const prefix = typeof options.prefix === "string" && options.prefix.length > 0 ? options.prefix : null;
|
|
23374
|
+
const escapedPrefix = prefix ? `${prefix.replace(/[\\%_]/g, (m) => `\\${m}`)}%` : null;
|
|
23375
|
+
const where = escapedPrefix ? `WHERE user_id = ? AND key LIKE ? ESCAPE '\\'` : `WHERE user_id = ?`;
|
|
23376
|
+
const whereArgs = escapedPrefix ? [userId, escapedPrefix] : [userId];
|
|
23377
|
+
const totalCursor = await this.ctx.storage.sql.exec(
|
|
23378
|
+
`SELECT COUNT(*) AS total FROM user_values ${where}`,
|
|
23379
|
+
...whereArgs
|
|
23380
|
+
);
|
|
23381
|
+
const total = Number(totalCursor.toArray()[0]?.total ?? 0);
|
|
23382
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
23383
|
+
`SELECT key, value, updated_at FROM user_values ${where}
|
|
23384
|
+
ORDER BY key ASC LIMIT ? OFFSET ?`,
|
|
23385
|
+
...whereArgs,
|
|
23386
|
+
limit,
|
|
23387
|
+
offset
|
|
23388
|
+
);
|
|
23389
|
+
const entries = cursor.toArray().map((row) => {
|
|
23390
|
+
let value = null;
|
|
23391
|
+
try {
|
|
23392
|
+
value = JSON.parse(row.value);
|
|
23393
|
+
} catch {
|
|
23394
|
+
value = null;
|
|
23395
|
+
}
|
|
23396
|
+
return { key: row.key, value, updated_at: Number(row.updated_at) };
|
|
23397
|
+
});
|
|
23398
|
+
return { entries, total, limit, offset };
|
|
23399
|
+
}
|
|
22868
23400
|
async getMissingRequiredScopedSubagentEnv(params) {
|
|
22869
23401
|
await this.ensureMigrated();
|
|
22870
23402
|
const requiredScoped = await this.getRequiredScopedVariableNamesForAgent(
|