@zq-silk/yui 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/cli/updateOrchestrator.js +12 -6
- package/dist/cli.js +12 -0
- package/dist/controller/agentRuntimeObserver.js +76 -16
- package/dist/controller/controller.js +398 -76
- package/dist/controller/fileSchedulerStoreAdapter.js +60 -51
- package/dist/controller/jobSupervisor.js +2 -16
- package/dist/controller/resourceInventoryLinux.js +73 -20
- package/dist/controller/runtime.js +1 -0
- package/dist/controller/runtimeEventProcessor.js +171 -31
- package/dist/coordination/keyedWorkQueue.js +189 -0
- package/dist/runtime/runtimeSessionCandidate.js +43 -0
- package/dist/scheduler/activeRoleRunDelivery.js +2 -4
- package/dist/scheduler/activeTaskProgress.js +2 -4
- package/dist/scheduler/leaderWakeupProcessor.js +3 -1
- package/dist/scheduler/ports.js +35 -2
- package/dist/scheduler/roleRunLiveness.js +44 -22
- package/dist/scheduler/roleRunStall.js +49 -25
- package/dist/storage/sqliteSchema.js +363 -14
- package/dist/storage/sqliteStore.js +403 -30
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/taskStore.js +104 -6
- package/i18n/README.zh-CN.md +7 -1
- package/package.json +1 -1
|
@@ -40,6 +40,8 @@ import { join } from "node:path";
|
|
|
40
40
|
import { isDeepStrictEqual } from "node:util";
|
|
41
41
|
import Database from "better-sqlite3";
|
|
42
42
|
import { mailboxTargetKey } from "../coordination/workMailbox.js";
|
|
43
|
+
import { validatePendingTurnCompletion } from "../executor/turnCompletion.js";
|
|
44
|
+
import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
|
|
43
45
|
import { validateReviewFinding } from "../review/reviewFinding.js";
|
|
44
46
|
import { generateHomeIdentity } from "../repository/homeIdentity.js";
|
|
45
47
|
import { validateIntegrationQueueEntry } from "../integration/integrationQueueEntry.js";
|
|
@@ -579,6 +581,11 @@ export class SqliteTaskStore {
|
|
|
579
581
|
if (sessions !== null) {
|
|
580
582
|
this.#db.prepare(`INSERT INTO global_role_session_sets (name, payload, updated_at) VALUES (?, ?, ?)
|
|
581
583
|
ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(role.name, this.#json(sessions), this.#now());
|
|
584
|
+
this.#saveRuntimeSessionCandidate(sessions);
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
this.#db.prepare("DELETE FROM global_role_session_sets WHERE name = ?").run(role.name);
|
|
588
|
+
this.#deleteRuntimeSessionCandidate({ scope: "global", roleName: role.name });
|
|
582
589
|
}
|
|
583
590
|
});
|
|
584
591
|
}
|
|
@@ -595,7 +602,11 @@ export class SqliteTaskStore {
|
|
|
595
602
|
return this.#getPayload("global_roles", "name = ?", [name]);
|
|
596
603
|
}
|
|
597
604
|
removeGlobalRole(name) {
|
|
598
|
-
return this.#mutate(() =>
|
|
605
|
+
return this.#mutate(() => {
|
|
606
|
+
this.#deleteRuntimeSessionCandidate({ scope: "global", roleName: name });
|
|
607
|
+
this.#db.prepare("DELETE FROM global_role_session_sets WHERE name = ?").run(name);
|
|
608
|
+
return this.#db.prepare("DELETE FROM global_roles WHERE name = ?").run(name).changes > 0;
|
|
609
|
+
});
|
|
599
610
|
}
|
|
600
611
|
getGlobalRoleSessionSet(name) {
|
|
601
612
|
return this.#getPayload("global_role_session_sets", "name = ?", [name]);
|
|
@@ -608,6 +619,7 @@ export class SqliteTaskStore {
|
|
|
608
619
|
this.#mutate(() => {
|
|
609
620
|
this.#db.prepare(`INSERT INTO global_role_session_sets (name, payload, updated_at) VALUES (?, ?, ?)
|
|
610
621
|
ON CONFLICT(name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.roleName, this.#json(sessions), this.#now());
|
|
622
|
+
this.#saveRuntimeSessionCandidate(sessions);
|
|
611
623
|
});
|
|
612
624
|
}
|
|
613
625
|
// -- tasks ------------------------------------------------------------------
|
|
@@ -800,8 +812,8 @@ export class SqliteTaskStore {
|
|
|
800
812
|
findDurableJobByIdempotencyKey(taskId, key) {
|
|
801
813
|
return this.#getPayload("durable_jobs", "task_id = ? AND idempotency_key = ?", [taskId, key]);
|
|
802
814
|
}
|
|
803
|
-
|
|
804
|
-
return this.#listPayload("durable_jobs", "
|
|
815
|
+
listActiveDurableJobs() {
|
|
816
|
+
return this.#sortById(this.#listPayload("durable_jobs", "status IN ('queued', 'running')", []), (job) => `${job.taskId}/${job.id}`);
|
|
805
817
|
}
|
|
806
818
|
hasActiveDurableJobs() {
|
|
807
819
|
const row = this.#db.prepare("SELECT 1 FROM durable_jobs WHERE status IN ('queued', 'running') LIMIT 1").get();
|
|
@@ -878,10 +890,19 @@ export class SqliteTaskStore {
|
|
|
878
890
|
ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.taskId, role.name, this.#json(role), this.#now());
|
|
879
891
|
this.#db.prepare(`INSERT INTO role_session_sets (task_id, role_name, payload, updated_at) VALUES (?, ?, ?, ?)
|
|
880
892
|
ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(sessions.owner.taskId, sessions.owner.roleName, this.#json(sessions), this.#now());
|
|
893
|
+
this.#saveRuntimeSessionCandidate(sessions);
|
|
894
|
+
this.#savePendingRuntimeTurnCompletion(sessions);
|
|
881
895
|
});
|
|
882
896
|
}
|
|
883
897
|
removeTaskRole(taskId, name) {
|
|
884
898
|
return this.#mutate(() => {
|
|
899
|
+
this.#deleteRuntimeSessionCandidate({
|
|
900
|
+
scope: "task",
|
|
901
|
+
taskId,
|
|
902
|
+
roleName: name
|
|
903
|
+
});
|
|
904
|
+
this.#db.prepare("DELETE FROM role_session_sets WHERE task_id = ? AND role_name = ?").run(taskId, name);
|
|
905
|
+
this.#db.prepare("DELETE FROM pending_runtime_turn_completions WHERE task_id = ? AND role_name = ?").run(taskId, name);
|
|
885
906
|
const result = this.#db.prepare("DELETE FROM task_roles WHERE task_id = ? AND role_name = ?").run(taskId, name);
|
|
886
907
|
return result.changes > 0;
|
|
887
908
|
});
|
|
@@ -936,12 +957,164 @@ export class SqliteTaskStore {
|
|
|
936
957
|
listRoleSessionSets(taskId) {
|
|
937
958
|
return this.#sortById(this.#listPayload("role_session_sets", "task_id = ?", [taskId]), (set) => set.owner.roleName);
|
|
938
959
|
}
|
|
960
|
+
listRuntimeSessionCandidates(query = {}) {
|
|
961
|
+
const taskIds = query.taskIds === undefined
|
|
962
|
+
? undefined
|
|
963
|
+
: [...new Set(query.taskIds)].sort(numericCompare);
|
|
964
|
+
if (taskIds?.length === 0 || (taskIds !== undefined && query.scope === "global")) {
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
967
|
+
const predicates = [];
|
|
968
|
+
const parameters = [];
|
|
969
|
+
if (query.cleanupRequiredOnly)
|
|
970
|
+
predicates.push("cleanup_required = 1");
|
|
971
|
+
if (taskIds !== undefined) {
|
|
972
|
+
predicates.push("scope = 'task'");
|
|
973
|
+
predicates.push(`task_id IN (${taskIds.map(() => "?").join(", ")})`);
|
|
974
|
+
parameters.push(...taskIds);
|
|
975
|
+
}
|
|
976
|
+
else if (query.scope !== undefined) {
|
|
977
|
+
predicates.push("scope = ?");
|
|
978
|
+
parameters.push(query.scope);
|
|
979
|
+
}
|
|
980
|
+
const where = predicates.length === 0
|
|
981
|
+
? ""
|
|
982
|
+
: ` WHERE ${predicates.join(" AND ")}`;
|
|
983
|
+
const rows = this.#db.prepare(`SELECT scope, task_id, role_name, agent_id, adapter_id,
|
|
984
|
+
native_session_id, launch_id, status, session_updated_at,
|
|
985
|
+
cleanup_required
|
|
986
|
+
FROM runtime_session_candidates${where}`).all(...parameters);
|
|
987
|
+
const candidates = rows.map((row) => ({
|
|
988
|
+
owner: row.scope === "task"
|
|
989
|
+
? { scope: "task", taskId: row.task_id, roleName: row.role_name }
|
|
990
|
+
: { scope: "global", roleName: row.role_name },
|
|
991
|
+
agentId: row.agent_id,
|
|
992
|
+
adapterId: row.adapter_id,
|
|
993
|
+
nativeSessionId: row.native_session_id,
|
|
994
|
+
...(row.launch_id === null ? {} : { launchId: row.launch_id }),
|
|
995
|
+
status: row.status,
|
|
996
|
+
sessionUpdatedAt: row.session_updated_at,
|
|
997
|
+
cleanupRequired: row.cleanup_required === 1
|
|
998
|
+
})).sort(compareRuntimeSessionCandidates);
|
|
999
|
+
for (const candidate of candidates) {
|
|
1000
|
+
if (!this.#runtimeSessionCandidateMatchesSource(candidate)) {
|
|
1001
|
+
throw new StorageRecordError(`Runtime Session projection is inconsistent: ${candidate.owner.scope === "task"
|
|
1002
|
+
? `${candidate.owner.taskId}/${candidate.owner.roleName}`
|
|
1003
|
+
: `global/${candidate.owner.roleName}`}.`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return candidates;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Compare the hot projection with the authoritative current Session without
|
|
1010
|
+
* parsing or validating the RoleSessionSet aggregate. In particular, the
|
|
1011
|
+
* historical `history` field must never enter this cleanup discovery path:
|
|
1012
|
+
* JSON1 reads only the active Agent object selected by `activeAgentId`.
|
|
1013
|
+
*/
|
|
1014
|
+
#runtimeSessionCandidateMatchesSource(candidate) {
|
|
1015
|
+
const source = candidate.owner.scope === "task"
|
|
1016
|
+
? {
|
|
1017
|
+
table: "role_session_sets",
|
|
1018
|
+
where: "task_id = ? AND role_name = ?",
|
|
1019
|
+
parameters: [candidate.owner.taskId, candidate.owner.roleName]
|
|
1020
|
+
}
|
|
1021
|
+
: {
|
|
1022
|
+
table: "global_role_session_sets",
|
|
1023
|
+
where: "name = ?",
|
|
1024
|
+
parameters: [candidate.owner.roleName]
|
|
1025
|
+
};
|
|
1026
|
+
let row;
|
|
1027
|
+
try {
|
|
1028
|
+
row = this.#db.prepare(`WITH source AS (
|
|
1029
|
+
SELECT payload,
|
|
1030
|
+
json_extract(payload, '$.activeAgentId') AS active_agent_id
|
|
1031
|
+
FROM ${source.table}
|
|
1032
|
+
WHERE ${source.where}
|
|
1033
|
+
), active AS (
|
|
1034
|
+
SELECT payload,
|
|
1035
|
+
active_agent_id,
|
|
1036
|
+
json_extract(
|
|
1037
|
+
payload,
|
|
1038
|
+
'$.sessions.' || json_quote(active_agent_id)
|
|
1039
|
+
) AS active_session
|
|
1040
|
+
FROM source
|
|
1041
|
+
)
|
|
1042
|
+
SELECT
|
|
1043
|
+
json_extract(payload, '$.owner.scope') AS owner_scope,
|
|
1044
|
+
json_extract(payload, '$.owner.taskId') AS owner_task_id,
|
|
1045
|
+
json_extract(payload, '$.owner.roleName') AS owner_role_name,
|
|
1046
|
+
active_agent_id,
|
|
1047
|
+
json_extract(active_session, '$.agentId') AS agent_id,
|
|
1048
|
+
json_extract(active_session, '$.adapterId') AS adapter_id,
|
|
1049
|
+
json_extract(active_session, '$.nativeSessionId') AS native_session_id,
|
|
1050
|
+
json_extract(active_session, '$.launchId') AS launch_id,
|
|
1051
|
+
json_extract(active_session, '$.status') AS status,
|
|
1052
|
+
json_extract(active_session, '$.updatedAt') AS session_updated_at,
|
|
1053
|
+
CASE
|
|
1054
|
+
WHEN json_extract(active_session, '$.status') NOT IN ('stopped','broken')
|
|
1055
|
+
AND (
|
|
1056
|
+
json_extract(active_session, '$.status') = 'running'
|
|
1057
|
+
OR json_type(active_session, '$.launchId') = 'text'
|
|
1058
|
+
)
|
|
1059
|
+
THEN 1 ELSE 0
|
|
1060
|
+
END AS cleanup_required
|
|
1061
|
+
FROM active`).get(...source.parameters);
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1065
|
+
throw new StorageRecordError(`Runtime Session projection source is invalid for ${candidate.owner.scope === "task"
|
|
1066
|
+
? `${candidate.owner.taskId}/${candidate.owner.roleName}`
|
|
1067
|
+
: `global/${candidate.owner.roleName}`}: ${detail}`);
|
|
1068
|
+
}
|
|
1069
|
+
if (row === undefined)
|
|
1070
|
+
return false;
|
|
1071
|
+
return row.owner_scope === candidate.owner.scope
|
|
1072
|
+
&& row.owner_task_id === (candidate.owner.scope === "task" ? candidate.owner.taskId : null)
|
|
1073
|
+
&& row.owner_role_name === candidate.owner.roleName
|
|
1074
|
+
&& row.active_agent_id === candidate.agentId
|
|
1075
|
+
&& row.agent_id === candidate.agentId
|
|
1076
|
+
&& row.adapter_id === candidate.adapterId
|
|
1077
|
+
&& row.native_session_id === candidate.nativeSessionId
|
|
1078
|
+
&& (row.launch_id ?? undefined) === candidate.launchId
|
|
1079
|
+
&& row.status === candidate.status
|
|
1080
|
+
&& row.session_updated_at === candidate.sessionUpdatedAt
|
|
1081
|
+
&& row.cleanup_required === (candidate.cleanupRequired ? 1 : 0);
|
|
1082
|
+
}
|
|
1083
|
+
listPendingRuntimeTurnCompletions(taskIds) {
|
|
1084
|
+
const selectedTaskIds = taskIds === undefined
|
|
1085
|
+
? undefined
|
|
1086
|
+
: [...new Set(taskIds)].sort(numericCompare);
|
|
1087
|
+
if (selectedTaskIds?.length === 0)
|
|
1088
|
+
return [];
|
|
1089
|
+
const where = selectedTaskIds === undefined
|
|
1090
|
+
? ""
|
|
1091
|
+
: ` WHERE task_id IN (${selectedTaskIds.map(() => "?").join(", ")})`;
|
|
1092
|
+
const rows = this.#db.prepare(`SELECT task_id, role_name, schema_version, agent_id, native_session_id,
|
|
1093
|
+
turn_id, run_id, summary, observed_at, due_at
|
|
1094
|
+
FROM pending_runtime_turn_completions${where}`).all(...(selectedTaskIds ?? []));
|
|
1095
|
+
return rows.map((row) => validatePendingTurnCompletion({
|
|
1096
|
+
schemaVersion: row.schema_version,
|
|
1097
|
+
taskId: row.task_id,
|
|
1098
|
+
roleName: row.role_name,
|
|
1099
|
+
agentId: row.agent_id,
|
|
1100
|
+
nativeSessionId: row.native_session_id,
|
|
1101
|
+
turnId: row.turn_id,
|
|
1102
|
+
runId: row.run_id,
|
|
1103
|
+
summary: row.summary,
|
|
1104
|
+
observedAt: row.observed_at,
|
|
1105
|
+
dueAt: row.due_at
|
|
1106
|
+
})).sort((left, right) => (numericCompare(left.taskId, right.taskId)
|
|
1107
|
+
|| numericCompare(left.roleName, right.roleName)
|
|
1108
|
+
|| numericCompare(left.runId, right.runId)));
|
|
1109
|
+
}
|
|
939
1110
|
saveRoleSessionSet(sessions) {
|
|
940
1111
|
const taskId = sessions.owner.taskId;
|
|
941
1112
|
this.#requireTask(taskId);
|
|
942
1113
|
this.#mutate(() => {
|
|
943
1114
|
this.#db.prepare(`INSERT INTO role_session_sets (task_id, role_name, payload, updated_at) VALUES (?, ?, ?, ?)
|
|
944
1115
|
ON CONFLICT(task_id, role_name) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, sessions.owner.roleName, this.#json(sessions), this.#now());
|
|
1116
|
+
this.#saveRuntimeSessionCandidate(sessions);
|
|
1117
|
+
this.#savePendingRuntimeTurnCompletion(sessions);
|
|
945
1118
|
});
|
|
946
1119
|
}
|
|
947
1120
|
saveTaskRoleSessionSet(sessions) {
|
|
@@ -954,6 +1127,56 @@ export class SqliteTaskStore {
|
|
|
954
1127
|
const session = set.sessions[set.activeAgentId];
|
|
955
1128
|
return session === undefined ? null : session;
|
|
956
1129
|
}
|
|
1130
|
+
#saveRuntimeSessionCandidate(sessions) {
|
|
1131
|
+
const candidate = projectRuntimeSessionCandidate(sessions);
|
|
1132
|
+
if (candidate === null) {
|
|
1133
|
+
this.#deleteRuntimeSessionCandidate(sessions.owner);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const taskId = candidate.owner.scope === "task" ? candidate.owner.taskId : "";
|
|
1137
|
+
this.#db.prepare(`INSERT INTO runtime_session_candidates (
|
|
1138
|
+
scope, task_id, role_name, agent_id, adapter_id, native_session_id,
|
|
1139
|
+
launch_id, status, session_updated_at, cleanup_required
|
|
1140
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1141
|
+
ON CONFLICT(scope, task_id, role_name) DO UPDATE SET
|
|
1142
|
+
agent_id = excluded.agent_id,
|
|
1143
|
+
adapter_id = excluded.adapter_id,
|
|
1144
|
+
native_session_id = excluded.native_session_id,
|
|
1145
|
+
launch_id = excluded.launch_id,
|
|
1146
|
+
status = excluded.status,
|
|
1147
|
+
session_updated_at = excluded.session_updated_at,
|
|
1148
|
+
cleanup_required = excluded.cleanup_required`).run(candidate.owner.scope, taskId, candidate.owner.roleName, candidate.agentId, candidate.adapterId, candidate.nativeSessionId, candidate.launchId ?? null, candidate.status, candidate.sessionUpdatedAt, candidate.cleanupRequired ? 1 : 0);
|
|
1149
|
+
}
|
|
1150
|
+
#deleteRuntimeSessionCandidate(owner) {
|
|
1151
|
+
this.#db.prepare(`DELETE FROM runtime_session_candidates
|
|
1152
|
+
WHERE scope = ? AND task_id = ? AND role_name = ?`).run(owner.scope, owner.scope === "task" ? owner.taskId : "", owner.roleName);
|
|
1153
|
+
}
|
|
1154
|
+
/** Maintain the independent pending-Turn projection in the same write txn. */
|
|
1155
|
+
#savePendingRuntimeTurnCompletion(sessions) {
|
|
1156
|
+
const owner = sessions.owner;
|
|
1157
|
+
const pending = sessions.pendingTurnCompletion;
|
|
1158
|
+
if (pending === null) {
|
|
1159
|
+
this.#db.prepare("DELETE FROM pending_runtime_turn_completions WHERE task_id = ? AND role_name = ?").run(owner.taskId, owner.roleName);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const normalized = validatePendingTurnCompletion(pending);
|
|
1163
|
+
if (normalized.taskId !== owner.taskId || normalized.roleName !== owner.roleName) {
|
|
1164
|
+
throw new StorageRecordError(`Pending Turn completion owner does not match Role session: ${owner.taskId}/${owner.roleName}.`);
|
|
1165
|
+
}
|
|
1166
|
+
this.#db.prepare(`INSERT INTO pending_runtime_turn_completions (
|
|
1167
|
+
task_id, role_name, schema_version, agent_id, native_session_id,
|
|
1168
|
+
turn_id, run_id, summary, observed_at, due_at
|
|
1169
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1170
|
+
ON CONFLICT(task_id, role_name) DO UPDATE SET
|
|
1171
|
+
schema_version = excluded.schema_version,
|
|
1172
|
+
agent_id = excluded.agent_id,
|
|
1173
|
+
native_session_id = excluded.native_session_id,
|
|
1174
|
+
turn_id = excluded.turn_id,
|
|
1175
|
+
run_id = excluded.run_id,
|
|
1176
|
+
summary = excluded.summary,
|
|
1177
|
+
observed_at = excluded.observed_at,
|
|
1178
|
+
due_at = excluded.due_at`).run(normalized.taskId, normalized.roleName, normalized.schemaVersion, normalized.agentId, normalized.nativeSessionId, normalized.turnId, normalized.runId, normalized.summary, normalized.observedAt, normalized.dueAt);
|
|
1179
|
+
}
|
|
957
1180
|
// -- work items -------------------------------------------------------------
|
|
958
1181
|
nextWorkItemId(taskId) { return this.#nextTaskRecordId(taskId, "workItem"); }
|
|
959
1182
|
getWorkItem(taskId, workItemId) {
|
|
@@ -1156,15 +1379,26 @@ export class SqliteTaskStore {
|
|
|
1156
1379
|
* replaces the adapter's per-Task in-memory sweep, so Controller deadline
|
|
1157
1380
|
* arming no longer materializes every Task and Run in JavaScript.
|
|
1158
1381
|
*/
|
|
1159
|
-
listPendingProviderRetries() {
|
|
1160
|
-
const
|
|
1382
|
+
listPendingProviderRetries(taskIds) {
|
|
1383
|
+
const selectedTaskIds = taskIds === undefined
|
|
1384
|
+
? undefined
|
|
1385
|
+
: [...new Set(taskIds)].sort(numericCompare);
|
|
1386
|
+
if (selectedTaskIds?.length === 0)
|
|
1387
|
+
return [];
|
|
1388
|
+
const taskPredicate = selectedTaskIds === undefined
|
|
1389
|
+
? ""
|
|
1390
|
+
: ` AND tc.task_id IN (${selectedTaskIds.map(() => "?").join(", ")})`;
|
|
1391
|
+
const rows = this.#db.prepare(`SELECT DISTINCT ar.task_id AS taskId, ar.run_id AS runId, ar.role_name AS roleName,
|
|
1161
1392
|
json_extract(ar.payload, '$.providerRetry.nextAttemptAt') AS nextAttemptAt
|
|
1162
|
-
FROM
|
|
1163
|
-
JOIN
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
AND
|
|
1167
|
-
|
|
1393
|
+
FROM tasks_catalog tc INDEXED BY idx_tasks_active
|
|
1394
|
+
JOIN active_runs ap ON ap.task_id = tc.task_id
|
|
1395
|
+
JOIN agent_runs ar ON ar.task_id = ap.task_id AND ar.run_id = ap.run_id
|
|
1396
|
+
WHERE tc.is_active = 1
|
|
1397
|
+
AND ar.status = 'active'
|
|
1398
|
+
AND json_extract(ar.payload, '$.providerRetry.nextAttemptAt') IS NOT NULL${taskPredicate}`).all(...(selectedTaskIds ?? []));
|
|
1399
|
+
return rows.sort((left, right) => (numericCompare(left.taskId, right.taskId)
|
|
1400
|
+
|| numericCompare(left.roleName, right.roleName)
|
|
1401
|
+
|| numericCompare(left.runId, right.runId)));
|
|
1168
1402
|
}
|
|
1169
1403
|
// -- review rounds ----------------------------------------------------------
|
|
1170
1404
|
nextReviewRoundId(taskId) { return this.#nextTaskRecordId(taskId, "reviewRound"); }
|
|
@@ -1213,11 +1447,82 @@ export class SqliteTaskStore {
|
|
|
1213
1447
|
ON CONFLICT(task_id, pointer) DO UPDATE SET run_id = excluded.run_id, payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, pointer, runId, payload, this.#now());
|
|
1214
1448
|
});
|
|
1215
1449
|
}
|
|
1216
|
-
#
|
|
1450
|
+
#readActiveRunPointer(taskId, pointer) {
|
|
1217
1451
|
const row = this.#db.prepare("SELECT run_id FROM active_runs WHERE task_id = ? AND pointer = ?").get(taskId, pointer);
|
|
1218
1452
|
if (row === undefined)
|
|
1219
1453
|
return null;
|
|
1220
|
-
return
|
|
1454
|
+
return {
|
|
1455
|
+
runId: row.run_id,
|
|
1456
|
+
run: this.getAgentRun(taskId, row.run_id)
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
#assertActiveRunForWrite(run, lane) {
|
|
1460
|
+
if (run.status !== "active") {
|
|
1461
|
+
throw new StorageRecordError(`Active Agent run must have active status: ${run.id}`);
|
|
1462
|
+
}
|
|
1463
|
+
const hasGroup = run.executionGroupId !== undefined;
|
|
1464
|
+
const hasLane = run.executionLaneId !== undefined;
|
|
1465
|
+
if (hasGroup !== hasLane) {
|
|
1466
|
+
throw new StorageRecordError(`Agent Run execution lineage is incomplete: ${run.id}.`);
|
|
1467
|
+
}
|
|
1468
|
+
if (lane !== undefined
|
|
1469
|
+
&& (run.executionGroupId !== lane.executionGroupId
|
|
1470
|
+
|| run.executionLaneId !== lane.executionLaneId)) {
|
|
1471
|
+
throw new StorageRecordError(`Active Agent run execution lineage does not match its Lane: ${run.id}`);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
#getActiveRoleRun(taskId, roleName) {
|
|
1475
|
+
const pointer = this.#readActiveRunPointer(taskId, roleName);
|
|
1476
|
+
if (pointer === null)
|
|
1477
|
+
return null;
|
|
1478
|
+
const task = this.getTask(taskId);
|
|
1479
|
+
if (task === null) {
|
|
1480
|
+
throw new StorageRecordError(`Active Agent run Task is missing: ${taskId}/${roleName}`);
|
|
1481
|
+
}
|
|
1482
|
+
if (pointer.run === null) {
|
|
1483
|
+
// Retired Tasks are an explicit historical isolation boundary. Their
|
|
1484
|
+
// retained pointer rows may intentionally reference a missing Run.
|
|
1485
|
+
if (task.status === "retired")
|
|
1486
|
+
return null;
|
|
1487
|
+
throw new StorageRecordError(`Active Agent run pointer is dangling: ${taskId}/${roleName}`);
|
|
1488
|
+
}
|
|
1489
|
+
const run = pointer.run;
|
|
1490
|
+
if (task.status === "retired")
|
|
1491
|
+
return run;
|
|
1492
|
+
if (run.id !== pointer.runId
|
|
1493
|
+
|| run.taskId !== taskId
|
|
1494
|
+
|| run.roleName !== roleName
|
|
1495
|
+
|| run.status !== "active"
|
|
1496
|
+
|| (run.executionGroupId === undefined) !== (run.executionLaneId === undefined)) {
|
|
1497
|
+
throw new StorageRecordError(`Active Agent run pointer is invalid: ${taskId}/${roleName}`);
|
|
1498
|
+
}
|
|
1499
|
+
return run;
|
|
1500
|
+
}
|
|
1501
|
+
#getActiveLaneRun(taskId, executionGroupId, executionLaneId) {
|
|
1502
|
+
const pointerKey = executionLaneActiveRunKey(executionGroupId, executionLaneId);
|
|
1503
|
+
const pointer = this.#readActiveRunPointer(taskId, pointerKey);
|
|
1504
|
+
if (pointer === null)
|
|
1505
|
+
return null;
|
|
1506
|
+
const task = this.getTask(taskId);
|
|
1507
|
+
if (task === null) {
|
|
1508
|
+
throw new StorageRecordError(`Active Agent run Task is missing: ${taskId}/${pointerKey}`);
|
|
1509
|
+
}
|
|
1510
|
+
if (pointer.run === null) {
|
|
1511
|
+
if (task.status === "retired")
|
|
1512
|
+
return null;
|
|
1513
|
+
throw new StorageRecordError(`Active Agent run pointer is dangling: ${taskId}/${pointerKey}`);
|
|
1514
|
+
}
|
|
1515
|
+
const run = pointer.run;
|
|
1516
|
+
if (task.status === "retired")
|
|
1517
|
+
return run;
|
|
1518
|
+
if (run.id !== pointer.runId
|
|
1519
|
+
|| run.taskId !== taskId
|
|
1520
|
+
|| run.status !== "active"
|
|
1521
|
+
|| run.executionGroupId !== executionGroupId
|
|
1522
|
+
|| run.executionLaneId !== executionLaneId) {
|
|
1523
|
+
throw new StorageRecordError(`Active Agent run pointer is invalid: ${taskId}/${pointerKey}`);
|
|
1524
|
+
}
|
|
1525
|
+
return run;
|
|
1221
1526
|
}
|
|
1222
1527
|
#clearActiveRun(taskId, pointer) {
|
|
1223
1528
|
this.#mutate(() => {
|
|
@@ -1225,44 +1530,94 @@ export class SqliteTaskStore {
|
|
|
1225
1530
|
});
|
|
1226
1531
|
}
|
|
1227
1532
|
getActiveAgentRun(taskId, roleName) {
|
|
1228
|
-
return this.#
|
|
1533
|
+
return this.#getActiveRoleRun(taskId, roleName);
|
|
1229
1534
|
}
|
|
1230
1535
|
saveActiveAgentRun(run) {
|
|
1231
|
-
if (run.executionGroupId !== undefined
|
|
1536
|
+
if (run.executionGroupId !== undefined || run.executionLaneId !== undefined) {
|
|
1232
1537
|
this.saveActiveExecutionLaneRun(run);
|
|
1233
1538
|
return;
|
|
1234
1539
|
}
|
|
1235
|
-
this
|
|
1540
|
+
this.transaction((store) => {
|
|
1541
|
+
this.#assertActiveRunForWrite(run);
|
|
1542
|
+
const current = store.getActiveAgentRun(run.taskId, run.roleName);
|
|
1543
|
+
if (current !== null && current.id !== run.id) {
|
|
1544
|
+
throw new StorageRecordError(`Role already has an active Agent run: ${run.taskId}/${run.roleName}`);
|
|
1545
|
+
}
|
|
1546
|
+
const sessions = store.getTaskRoleSessionSet(run.taskId, run.roleName);
|
|
1547
|
+
if (sessions !== null && sessions.inFlight !== null && sessions.inFlight.runId !== run.id) {
|
|
1548
|
+
throw new StorageRecordError(`Role still has an in-flight Turn: ${run.taskId}/${run.roleName}/${sessions.inFlight.runId}`);
|
|
1549
|
+
}
|
|
1550
|
+
// Keep the Run row and its active pointer in the same transaction. The
|
|
1551
|
+
// adapter may have already written the row; this upsert remains
|
|
1552
|
+
// intentionally idempotent for that backend-neutral path.
|
|
1553
|
+
store.saveAgentRun(run);
|
|
1554
|
+
this.#saveActiveRun(run.taskId, run.roleName, run.id);
|
|
1555
|
+
});
|
|
1236
1556
|
}
|
|
1237
1557
|
clearActiveAgentRun(taskId, roleName) {
|
|
1238
1558
|
// Older Controller paths only know the Role key. When that key points
|
|
1239
1559
|
// at a lane-backed Run, remove the matching lane pointer too; preserve
|
|
1240
1560
|
// every other lane for the same Role in a multi-lane group.
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1561
|
+
this.transaction(() => {
|
|
1562
|
+
const rolePointer = this.#readActiveRunPointer(taskId, roleName);
|
|
1563
|
+
this.#clearActiveRun(taskId, roleName);
|
|
1564
|
+
if (rolePointer === null)
|
|
1565
|
+
return;
|
|
1566
|
+
const laneRows = this.#db.prepare("SELECT pointer, run_id FROM active_runs WHERE task_id = ?").all(taskId);
|
|
1567
|
+
for (const row of laneRows) {
|
|
1568
|
+
if (executionLaneActiveRunKeyParts(row.pointer) !== null
|
|
1569
|
+
&& row.run_id === rolePointer.runId) {
|
|
1250
1570
|
this.#clearActiveRun(taskId, row.pointer);
|
|
1251
1571
|
}
|
|
1252
1572
|
}
|
|
1253
|
-
}
|
|
1573
|
+
});
|
|
1254
1574
|
}
|
|
1255
1575
|
getActiveExecutionLaneRun(taskId, executionGroupId, executionLaneId) {
|
|
1256
|
-
return this.#
|
|
1576
|
+
return this.#getActiveLaneRun(taskId, executionGroupId, executionLaneId);
|
|
1257
1577
|
}
|
|
1258
1578
|
saveActiveExecutionLaneRun(run) {
|
|
1259
1579
|
if (run.executionGroupId === undefined || run.executionLaneId === undefined) {
|
|
1260
1580
|
throw new StorageRecordError(`Active execution-lane run requires group and lane ids: ${run.id}`);
|
|
1261
1581
|
}
|
|
1262
|
-
this
|
|
1582
|
+
this.transaction((store) => {
|
|
1583
|
+
const key = executionLaneActiveRunKey(run.executionGroupId, run.executionLaneId);
|
|
1584
|
+
this.#assertActiveRunForWrite(run, {
|
|
1585
|
+
executionGroupId: run.executionGroupId,
|
|
1586
|
+
executionLaneId: run.executionLaneId
|
|
1587
|
+
});
|
|
1588
|
+
const current = store.getActiveExecutionLaneRun(run.taskId, run.executionGroupId, run.executionLaneId);
|
|
1589
|
+
if (current !== null && current.id !== run.id) {
|
|
1590
|
+
throw new StorageRecordError(`Execution Lane already has an active Agent run: ${run.taskId}/${key}`);
|
|
1591
|
+
}
|
|
1592
|
+
const sessions = store.getTaskRoleSessionSet(run.taskId, run.roleName);
|
|
1593
|
+
if (sessions !== null && sessions.inFlight !== null && sessions.inFlight.runId !== run.id) {
|
|
1594
|
+
throw new StorageRecordError(`Role still has an in-flight Turn: ${run.taskId}/${run.roleName}/${sessions.inFlight.runId}`);
|
|
1595
|
+
}
|
|
1596
|
+
const rolePointer = this.#readActiveRunPointer(run.taskId, run.roleName);
|
|
1597
|
+
store.saveAgentRun(run);
|
|
1598
|
+
this.#saveActiveRun(run.taskId, key, run.id);
|
|
1599
|
+
// Keep the legacy role pointer for the single-lane delivery path, but
|
|
1600
|
+
// never replace it when another Lane already owns that Role.
|
|
1601
|
+
if (rolePointer === null) {
|
|
1602
|
+
this.#saveActiveRun(run.taskId, run.roleName, run.id);
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1263
1605
|
}
|
|
1264
1606
|
clearActiveExecutionLaneRun(taskId, executionGroupId, executionLaneId) {
|
|
1265
|
-
this
|
|
1607
|
+
this.transaction(() => {
|
|
1608
|
+
const key = executionLaneActiveRunKey(executionGroupId, executionLaneId);
|
|
1609
|
+
const lanePointer = this.#readActiveRunPointer(taskId, key);
|
|
1610
|
+
this.#clearActiveRun(taskId, key);
|
|
1611
|
+
if (lanePointer === null)
|
|
1612
|
+
return;
|
|
1613
|
+
const roleRows = this.#db.prepare("SELECT pointer, run_id FROM active_runs WHERE task_id = ?").all(taskId);
|
|
1614
|
+
for (const row of roleRows) {
|
|
1615
|
+
if (executionLaneActiveRunKeyParts(row.pointer) === null
|
|
1616
|
+
&& row.run_id === lanePointer.runId) {
|
|
1617
|
+
this.#clearActiveRun(taskId, row.pointer);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1266
1621
|
}
|
|
1267
1622
|
// -- messages ----------------------------------------------------------------
|
|
1268
1623
|
nextMessageId(taskId) { return this.#nextTaskRecordId(taskId, "message"); }
|
|
@@ -1296,6 +1651,17 @@ export class SqliteTaskStore {
|
|
|
1296
1651
|
listInputRequests(taskId) {
|
|
1297
1652
|
return this.#sortById(this.#listPayload("input_requests", "task_id = ?", [taskId]), (request) => request.id);
|
|
1298
1653
|
}
|
|
1654
|
+
listOpenInputRequests(taskIds) {
|
|
1655
|
+
const selectedTaskIds = taskIds === undefined
|
|
1656
|
+
? undefined
|
|
1657
|
+
: [...new Set(taskIds)].sort(numericCompare);
|
|
1658
|
+
if (selectedTaskIds?.length === 0)
|
|
1659
|
+
return [];
|
|
1660
|
+
const where = selectedTaskIds === undefined
|
|
1661
|
+
? "status = 'open'"
|
|
1662
|
+
: `status = 'open' AND task_id IN (${selectedTaskIds.map(() => "?").join(", ")})`;
|
|
1663
|
+
return this.#sortById(this.#listPayload("input_requests", where, selectedTaskIds ?? []), (request) => `${request.taskId}/${request.id}`);
|
|
1664
|
+
}
|
|
1299
1665
|
listAllInputRequests() {
|
|
1300
1666
|
return this.#sortById(this.#listPayload("input_requests", "1=1", []), (request) => `${request.taskId}/${request.id}`);
|
|
1301
1667
|
}
|
|
@@ -1413,6 +1779,13 @@ export class SqliteTaskStore {
|
|
|
1413
1779
|
const rows = this.#db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending FROM mailboxes ORDER BY target_key").all();
|
|
1414
1780
|
return rows.map((row) => this.#rowToMailbox(row));
|
|
1415
1781
|
}
|
|
1782
|
+
listReadyWorkMailboxes() {
|
|
1783
|
+
const rows = this.#db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending
|
|
1784
|
+
FROM mailboxes
|
|
1785
|
+
WHERE processing IS NOT NULL OR pending IS NOT NULL
|
|
1786
|
+
ORDER BY target_key`).all();
|
|
1787
|
+
return rows.map((row) => this.#rowToMailbox(row));
|
|
1788
|
+
}
|
|
1416
1789
|
saveWorkMailbox(mailbox) {
|
|
1417
1790
|
const cols = this.#mailboxCols(mailbox.target);
|
|
1418
1791
|
this.#mutate(() => {
|
|
@@ -1500,7 +1873,7 @@ export class SqliteTaskStore {
|
|
|
1500
1873
|
return pendingWakeupProjection(this.getWorkMailbox({ kind: "role", taskId, roleName: "leader" }));
|
|
1501
1874
|
}
|
|
1502
1875
|
listPendingWakeups() {
|
|
1503
|
-
return this.
|
|
1876
|
+
return this.listReadyWorkMailboxes()
|
|
1504
1877
|
.flatMap((mailbox) => {
|
|
1505
1878
|
const wakeup = pendingWakeupProjection(mailbox);
|
|
1506
1879
|
return wakeup === null ? [] : [wakeup];
|
package/dist/storage/storeRpc.js
CHANGED
|
@@ -55,6 +55,7 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
55
55
|
"getChangeSet",
|
|
56
56
|
"listIntegrationAttempts",
|
|
57
57
|
"getIntegrationAttempt",
|
|
58
|
+
"listActiveDurableJobs",
|
|
58
59
|
"listRoles",
|
|
59
60
|
"getRole",
|
|
60
61
|
"listManagedWorkspaces",
|
|
@@ -67,6 +68,8 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
67
68
|
"getRoleSessionSet",
|
|
68
69
|
"getTaskRoleSessionSet",
|
|
69
70
|
"listRoleSessionSets",
|
|
71
|
+
"listRuntimeSessionCandidates",
|
|
72
|
+
"listPendingRuntimeTurnCompletions",
|
|
70
73
|
"getRoleSession",
|
|
71
74
|
"getWorkItem",
|
|
72
75
|
"listWorkItems",
|
|
@@ -81,6 +84,7 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
81
84
|
"listMessages",
|
|
82
85
|
"getInputRequest",
|
|
83
86
|
"listInputRequests",
|
|
87
|
+
"listOpenInputRequests",
|
|
84
88
|
"listAllInputRequests",
|
|
85
89
|
"listDecisions",
|
|
86
90
|
"getDecision",
|
|
@@ -89,6 +93,7 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
89
93
|
"listEvents",
|
|
90
94
|
"getWorkMailbox",
|
|
91
95
|
"listWorkMailboxes",
|
|
96
|
+
"listReadyWorkMailboxes",
|
|
92
97
|
"getPendingWakeup",
|
|
93
98
|
"listPendingWakeups",
|
|
94
99
|
"getLeaderFailure",
|