Entities.vala 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. using InvercargillSql.Orm;
  2. namespace ClankerSaddle.Data {
  3. /**
  4. * A software project worked on by saddle agents, stored in the
  5. * `projects` table.
  6. */
  7. public class Project : Object {
  8. public int64 id { get; set; }
  9. public string name { get; set; default = ""; }
  10. public string git_url { get; set; default = ""; }
  11. public string default_branch { get; set; default = "main"; }
  12. public string? summary { get; set; default = null; }
  13. public DateTime? created_at { get; set; default = null; }
  14. public static void entity_mapping(EntityMapperBuilder<Project> cfg) {
  15. cfg.table("projects")
  16. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  17. .column<string>("name", o => o.name, (o, v) => o.name = v)
  18. .column<string>("git_url", o => o.git_url, (o, v) => o.git_url = v)
  19. .column<string>("default_branch", o => o.default_branch, (o, v) => o.default_branch = v)
  20. .column<string>("summary", o => o.summary, (o, v) => o.summary = v)
  21. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  22. }
  23. }
  24. /**
  25. * A directed relationship between two projects ('co-developed' or
  26. * 'reference'), stored in the `project_links` table.
  27. */
  28. public class ProjectLink : Object {
  29. public int64 id { get; set; }
  30. public int64 parent_project_id { get; set; }
  31. public int64 linked_project_id { get; set; }
  32. public string relationship { get; set; default = "reference"; }
  33. public static void entity_mapping(EntityMapperBuilder<ProjectLink> cfg) {
  34. cfg.table("project_links")
  35. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  36. .column<int64?>("parent_project_id", o => o.parent_project_id, (o, v) => o.parent_project_id = v)
  37. .column<int64?>("linked_project_id", o => o.linked_project_id, (o, v) => o.linked_project_id = v)
  38. .column<string>("relationship", o => o.relationship, (o, v) => o.relationship = v);
  39. }
  40. }
  41. /**
  42. * A unit of work for a saddle agent: created in {@link
  43. * TaskService.STATUS_DRAFT}, queued by its creator (human or goal
  44. * agent), dispatched to a saddle, worked, possibly blocked on
  45. * questions, and eventually done or failed (completion is
  46. * agent-only). The plan-12 shape: goals own the task graph — a task
  47. * links its {@link goal_id}, runs as a plain worker or the goal
  48. * agent itself ({@link kind}), carries a work {@link mode} and its
  49. * {@link creator}, and human tasks may carry an auto-enqueue
  50. * predecessor ({@link enqueue_after_task_id}). Stored in the
  51. * `tasks` table. Named `TaskEntity` (not `Task`) to stay clear of
  52. * `GLib.Task` in files that import Gio transitively.
  53. */
  54. public class TaskEntity : Object {
  55. public int64 id { get; set; }
  56. public int64 project_id { get; set; }
  57. public string title { get; set; default = ""; }
  58. public string? description { get; set; default = null; }
  59. public string status { get; set; default = "draft"; }
  60. public int64 priority { get; set; default = 5; }
  61. public string? summary { get; set; default = null; }
  62. public string? task_branch { get; set; default = null; }
  63. public string? branch_from { get; set; default = null; }
  64. public DateTime? created_at { get; set; default = null; }
  65. public DateTime? started_at { get; set; default = null; }
  66. public DateTime? completed_at { get; set; default = null; }
  67. public DateTime? suspended_at { get; set; default = null; }
  68. /**
  69. * Goal 4 task 1 (Force Wake): the human pressed Force wake on
  70. * the task page — this suspended plain task becomes
  71. * parked-offerable again with the explicit wake notice.
  72. * Consumed (cleared) by the allocation that resumes it.
  73. */
  74. public bool force_wake { get; set; default = false; }
  75. /**
  76. * Goal 27 refinements (the parking guard): the ONE-SHOT
  77. * resume instruction a refused `saddle suspend` leaves on a
  78. * goal run that owned no live task (nothing could ever wake
  79. * the park). While set, the suspended run is immediately
  80. * resume-offerable; the allocation that hands the assignment
  81. * out consumes it (it becomes the assignment's
  82. * resume_notice, the force-wake discipline).
  83. */
  84. public string? pending_resume_notice { get; set; default = null; }
  85. /**
  86. * Goal 28 task D2: this task's lineage already spent its
  87. * turn-cap checkpoint-park (the daemon's first cap hit parks
  88. * with reason `turn_cap` instead of failing). The flag rides
  89. * EVERY later assignment (`turn_cap_parked`) so the daemon
  90. * hard-kills the next cap hit; it is never cleared — the
  91. * wrap-up chance is once per task.
  92. */
  93. public bool turn_cap_parked { get; set; default = false; }
  94. /** The goal this task belongs to (0 = standalone). */
  95. public int64 goal_id { get; set; default = 0; }
  96. /** "task" (a worker) or "goal_run" (the goal agent's own run). */
  97. public string kind { get; set; default = "task"; }
  98. /** implement / investigate / review / test (orchestrate for goal runs). */
  99. public string mode { get; set; default = "implement"; }
  100. /** "human" or "goal" — enqueueing is creator-only. */
  101. public string creator { get; set; default = "human"; }
  102. /** Human-created tasks only: auto-enqueue when this predecessor is done. */
  103. public int64 enqueue_after_task_id { get; set; default = 0; }
  104. public static void entity_mapping(EntityMapperBuilder<TaskEntity> cfg) {
  105. cfg.table("tasks")
  106. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  107. .column<int64?>("project_id", o => o.project_id, (o, v) => o.project_id = v)
  108. .column<string>("title", o => o.title, (o, v) => o.title = v)
  109. .column<string>("description", o => o.description, (o, v) => o.description = v)
  110. .column<string>("status", o => o.status, (o, v) => o.status = v)
  111. .column<int64?>("priority", o => o.priority, (o, v) => o.priority = v)
  112. .column<string>("summary", o => o.summary, (o, v) => o.summary = v)
  113. .column<string>("task_branch", o => o.task_branch, (o, v) => o.task_branch = v)
  114. .column<string>("branch_from", o => o.branch_from, (o, v) => o.branch_from = v)
  115. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v)
  116. .column<DateTime?>("started_at", o => o.started_at, (o, v) => o.started_at = v)
  117. .column<DateTime?>("completed_at", o => o.completed_at, (o, v) => o.completed_at = v)
  118. .column<DateTime?>("suspended_at", o => o.suspended_at, (o, v) => o.suspended_at = v)
  119. .column<bool>("force_wake", o => o.force_wake, (o, v) => o.force_wake = v)
  120. .column<string>("pending_resume_notice", o => o.pending_resume_notice, (o, v) => o.pending_resume_notice = v)
  121. .column<bool>("turn_cap_parked", o => o.turn_cap_parked, (o, v) => o.turn_cap_parked = v)
  122. .column<int64?>("goal_id", o => o.goal_id, (o, v) => o.goal_id = v ?? 0)
  123. .column<string>("kind", o => o.kind, (o, v) => o.kind = v)
  124. .column<string>("mode", o => o.mode, (o, v) => o.mode = v)
  125. .column<string>("creator", o => o.creator, (o, v) => o.creator = v)
  126. .column<int64?>("enqueue_after_task_id", o => o.enqueue_after_task_id, (o, v) => o.enqueue_after_task_id = v ?? 0);
  127. }
  128. }
  129. /**
  130. * A named bundle of reference/co-developed projects (goal 8 task
  131. * 2): applied one-click to a project's or a goal's link set so a
  132. * fleet's common reference collection needs one click, not N.
  133. * Stored in the `stacks` table.
  134. */
  135. public class Stack : Object {
  136. public int64 id { get; set; }
  137. public string name { get; set; default = ""; }
  138. public string? summary { get; set; default = null; }
  139. public DateTime? created_at { get; set; default = null; }
  140. public static void entity_mapping(EntityMapperBuilder<Stack> cfg) {
  141. cfg.table("stacks")
  142. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  143. .column<string>("name", o => o.name, (o, v) => o.name = v)
  144. .column<string>("summary", o => o.summary, (o, v) => o.summary = v)
  145. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  146. }
  147. }
  148. /**
  149. * One project of a {@link Stack} with the relationship it is
  150. * applied as ('co-developed' or 'reference'). Stored in the
  151. * `stack_projects` table.
  152. */
  153. public class StackProject : Object {
  154. public int64 id { get; set; }
  155. public int64 stack_id { get; set; }
  156. public int64 project_id { get; set; }
  157. public string relationship { get; set; default = "reference"; }
  158. public DateTime? created_at { get; set; default = null; }
  159. public static void entity_mapping(EntityMapperBuilder<StackProject> cfg) {
  160. cfg.table("stack_projects")
  161. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  162. .column<int64?>("stack_id", o => o.stack_id, (o, v) => o.stack_id = v)
  163. .column<int64?>("project_id", o => o.project_id, (o, v) => o.project_id = v)
  164. .column<string>("relationship", o => o.relationship, (o, v) => o.relationship = v)
  165. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  166. }
  167. }
  168. /**
  169. * One row of the plan-17 unified event ledger (`events`): daemon
  170. * events carry their session and its per-session monotonic `seq`
  171. * (the idempotency key), web-originated events carry neither. The
  172. * payload is a JSON object whose shape depends on `kind` (see
  173. * Services.EventService).
  174. */
  175. public class EventRecord : Object {
  176. public int64 id { get; set; }
  177. public int64 session_id { get; set; default = 0; }
  178. public int64 seq { get; set; default = 0; }
  179. public string kind { get; set; default = ""; }
  180. public string actor { get; set; default = ""; }
  181. public string scope_type { get; set; default = ""; }
  182. public int64 scope_id { get; set; }
  183. public string payload { get; set; default = "{}"; }
  184. public DateTime? created_at { get; set; default = null; }
  185. public static void entity_mapping(EntityMapperBuilder<EventRecord> cfg) {
  186. cfg.table("events")
  187. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  188. .column<int64?>("session_id",
  189. o => o.session_id > 0 ? (int64?) o.session_id : (int64?) null,
  190. (o, v) => o.session_id = v ?? 0)
  191. .column<int64?>("seq",
  192. o => o.seq > 0 ? (int64?) o.seq : (int64?) null,
  193. (o, v) => o.seq = v ?? 0)
  194. .column<string>("kind", o => o.kind, (o, v) => o.kind = v)
  195. .column<string>("actor", o => o.actor, (o, v) => o.actor = v)
  196. .column<string>("scope_type", o => o.scope_type, (o, v) => o.scope_type = v)
  197. .column<int64?>("scope_id", o => o.scope_id, (o, v) => o.scope_id = v ?? 0)
  198. .column<string>("payload", o => o.payload, (o, v) => o.payload = v)
  199. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  200. }
  201. }
  202. /** A remark on a task, authored either by a human or by the agent ('agent'). */
  203. public class TaskComment : Object {
  204. public int64 id { get; set; }
  205. public int64 task_id { get; set; }
  206. public string author { get; set; default = "agent"; }
  207. public string body { get; set; default = ""; }
  208. public string? quote { get; set; default = null; }
  209. public DateTime? created_at { get; set; default = null; }
  210. public static void entity_mapping(EntityMapperBuilder<TaskComment> cfg) {
  211. cfg.table("task_comments")
  212. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  213. .column<int64?>("task_id", o => o.task_id, (o, v) => o.task_id = v)
  214. .column<string>("author", o => o.author, (o, v) => o.author = v)
  215. .column<string>("body", o => o.body, (o, v) => o.body = v)
  216. .column<string>("quote", o => o.quote, (o, v) => o.quote = v)
  217. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  218. }
  219. }
  220. /**
  221. * A blocking prompt raised by an agent mid-task — a multiple-choice
  222. * {@link block_type} question, a request for comment or a review
  223. * request — answered by a human in the web UI; the answer is collected
  224. * by the saddle on its next
  225. * {@link ClankerSaddle.Services.SaddlePollState} delivery ledger poll.
  226. */
  227. public class TaskBlock : Object {
  228. public int64 id { get; set; }
  229. public int64 task_id { get; set; }
  230. public string question { get; set; default = ""; }
  231. public string? answer { get; set; default = null; }
  232. public string block_type { get; set; default = "question"; }
  233. public string? options { get; set; default = null; }
  234. public string? body { get; set; default = null; }
  235. public DateTime? answered_at { get; set; default = null; }
  236. public string? answered_by { get; set; default = null; }
  237. public DateTime? created_at { get; set; default = null; }
  238. public static void entity_mapping(EntityMapperBuilder<TaskBlock> cfg) {
  239. cfg.table("task_blocks")
  240. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  241. .column<int64?>("task_id", o => o.task_id, (o, v) => o.task_id = v)
  242. .column<string>("question", o => o.question, (o, v) => o.question = v)
  243. .column<string>("answer", o => o.answer, (o, v) => o.answer = v)
  244. .column<string>("type", o => o.block_type, (o, v) => o.block_type = v)
  245. .column<string>("options", o => o.options, (o, v) => o.options = v)
  246. .column<string>("body", o => o.body, (o, v) => o.body = v)
  247. .column<DateTime?>("answered_at", o => o.answered_at, (o, v) => o.answered_at = v)
  248. .column<string>("answered_by", o => o.answered_by, (o, v) => o.answered_by = v)
  249. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  250. }
  251. }
  252. /**
  253. * A saddle container registered with this app: one daemon managing one
  254. * bash session at a time, controlled over HTTP at {@link host}. The
  255. * per-saddle token ({@link token}, plan 10) authorises every
  256. * daemon→web call; removing the row revokes it. Plan 16 item 23:
  257. * sessions are the source of truth for "who runs what" — busy/idle
  258. * are DERIVED (busy iff an open session on an assigned/working task
  259. * exists; see {@link Services.SaddleService.current_work_view}) and
  260. * the row's `status` column is only the OFFLINE probe state
  261. * ('offline' vs 'idle'): the poller's unreachable stamp, cleared by
  262. * a successful probe or a proof-of-life ask. The former
  263. * `current_task_id`/`allocated_at` cache columns are gone.
  264. */
  265. public class Saddle : Object {
  266. public int64 id { get; set; }
  267. public string name { get; set; default = ""; }
  268. public string host { get; set; default = ""; }
  269. public string status { get; set; default = "idle"; }
  270. public string? git_name { get; set; default = null; }
  271. public string? git_email { get; set; default = null; }
  272. /**
  273. * The saddle admin's standing prompt note (goal 8 task 5):
  274. * a message about THIS machine (e.g. "You have root and may
  275. * install development tooling via DNF") served verbatim inside
  276. * every assignment's system prompt; null when unset.
  277. */
  278. public string? prompt_note { get; set; default = null; }
  279. public string? ssh_public_key { get; set; default = null; }
  280. public string? token { get; set; default = null; }
  281. /**
  282. * The workspace root this saddle's daemon reported at
  283. * /register (its SADDLE_WORKSPACE_ROOT), or null for daemons
  284. * that predate the field — assignment paths fall back to
  285. * SADDLE_PROJECTS_ROOT, then "/workspace".
  286. */
  287. public string? workspace_root { get; set; default = null; }
  288. public static void entity_mapping(EntityMapperBuilder<Saddle> cfg) {
  289. cfg.table("saddles")
  290. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  291. .column<string>("name", o => o.name, (o, v) => o.name = v)
  292. .column<string>("host", o => o.host, (o, v) => o.host = v)
  293. .column<string>("status", o => o.status, (o, v) => o.status = v)
  294. .column<string>("git_name", o => o.git_name, (o, v) => o.git_name = v)
  295. .column<string>("git_email", o => o.git_email, (o, v) => o.git_email = v)
  296. .column<string>("prompt_note", o => o.prompt_note, (o, v) => o.prompt_note = v)
  297. .column<string>("ssh_public_key", o => o.ssh_public_key, (o, v) => o.ssh_public_key = v)
  298. .column<string>("token", o => o.token, (o, v) => o.token = v)
  299. .column<string>("workspace_root", o => o.workspace_root, (o, v) => o.workspace_root = v);
  300. }
  301. }
  302. /**
  303. * One run of a task on a saddle: opened when the dispatcher assigns
  304. * the task, closed with an outcome when the session ends.
  305. */
  306. public class Session : Object {
  307. public int64 id { get; set; }
  308. public int64 task_id { get; set; }
  309. public int64 saddle_id { get; set; }
  310. public DateTime? started_at { get; set; default = null; }
  311. public DateTime? ended_at { get; set; default = null; }
  312. public string? outcome { get; set; default = null; }
  313. /**
  314. * What a resumed session's agent first received (plan 14 B4):
  315. * the resume_notice the allocation built, stored on the row at
  316. * resume-allocation; null for fresh sessions. Goal 28 task F:
  317. * `resume_since` records the batched-verdict watermark (the
  318. * prior round's session start) so the FINAL resume can
  319. * re-compose everything still undelivered, and
  320. * `answers_injected` counts verdicts the busy-answer channel
  321. * delivered mid-session (the follow-up park's notice
  322. * summarises instead of repeating them).
  323. */
  324. public string? resume_notice { get; set; default = null; }
  325. public DateTime? resume_since { get; set; default = null; }
  326. public int64 answers_injected { get; set; default = 0; }
  327. public static void entity_mapping(EntityMapperBuilder<Session> cfg) {
  328. cfg.table("sessions")
  329. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  330. .column<int64?>("task_id", o => o.task_id, (o, v) => o.task_id = v)
  331. .column<int64?>("saddle_id", o => o.saddle_id, (o, v) => o.saddle_id = v)
  332. .column<DateTime?>("started_at", o => o.started_at, (o, v) => o.started_at = v)
  333. .column<DateTime?>("ended_at", o => o.ended_at, (o, v) => o.ended_at = v)
  334. .column<string>("outcome", o => o.outcome, (o, v) => o.outcome = v)
  335. .column<string>("resume_notice", o => o.resume_notice, (o, v) => o.resume_notice = v)
  336. .column<DateTime?>("resume_since", o => o.resume_since, (o, v) => o.resume_since = v)
  337. .column<int64?>("answers_injected", o => o.answers_injected, (o, v) => o.answers_injected = v ?? 0);
  338. }
  339. }
  340. /**
  341. * A reusable instruction the dispatched agent must follow, stored in
  342. * the `rules` table and attached to projects and/or tasks.
  343. * `updated_at` stamps the last body/name edit (null until edited)
  344. * — the resume-time context_update delta keys on it.
  345. */
  346. public class Rule : Object {
  347. public int64 id { get; set; }
  348. public string name { get; set; default = ""; }
  349. public string body { get; set; default = ""; }
  350. public DateTime? created_at { get; set; default = null; }
  351. public DateTime? updated_at { get; set; default = null; }
  352. public static void entity_mapping(EntityMapperBuilder<Rule> cfg) {
  353. cfg.table("rules")
  354. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  355. .column<string>("name", o => o.name, (o, v) => o.name = v)
  356. .column<string>("body", o => o.body, (o, v) => o.body = v)
  357. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v)
  358. .column<DateTime?>("updated_at", o => o.updated_at, (o, v) => o.updated_at = v);
  359. }
  360. }
  361. /** An attachment of a rule to a project (`rule_projects`). */
  362. public class RuleProject : Object {
  363. public int64 id { get; set; }
  364. public int64 rule_id { get; set; }
  365. public int64 project_id { get; set; }
  366. public DateTime? created_at { get; set; default = null; }
  367. public static void entity_mapping(EntityMapperBuilder<RuleProject> cfg) {
  368. cfg.table("rule_projects")
  369. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  370. .column<int64?>("rule_id", o => o.rule_id, (o, v) => o.rule_id = v)
  371. .column<int64?>("project_id", o => o.project_id, (o, v) => o.project_id = v)
  372. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  373. }
  374. }
  375. /** An attachment of a rule to a task (`task_rules`). */
  376. public class TaskRule : Object {
  377. public int64 id { get; set; }
  378. public int64 rule_id { get; set; }
  379. public int64 task_id { get; set; }
  380. public DateTime? created_at { get; set; default = null; }
  381. public static void entity_mapping(EntityMapperBuilder<TaskRule> cfg) {
  382. cfg.table("task_rules")
  383. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  384. .column<int64?>("rule_id", o => o.rule_id, (o, v) => o.rule_id = v)
  385. .column<int64?>("task_id", o => o.task_id, (o, v) => o.task_id = v)
  386. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  387. }
  388. }
  389. /**
  390. * An LLM-authored remark about a project (`saddle note` via the
  391. * callback endpoint), curated from the project page. The source task
  392. * is null for notes without one.
  393. */
  394. public class ProjectNote : Object {
  395. public int64 id { get; set; }
  396. public int64 project_id { get; set; }
  397. public int64 source_task_id { get; set; default = 0; }
  398. public string body { get; set; default = ""; }
  399. public DateTime? created_at { get; set; default = null; }
  400. public static void entity_mapping(EntityMapperBuilder<ProjectNote> cfg) {
  401. cfg.table("project_notes")
  402. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  403. .column<int64?>("project_id", o => o.project_id, (o, v) => o.project_id = v)
  404. .column<int64?>("source_task_id", o => o.source_task_id, (o, v) => o.source_task_id = v ?? 0)
  405. .column<string>("body", o => o.body, (o, v) => o.body = v)
  406. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  407. }
  408. }
  409. /**
  410. * One historical revision of a project's Understanding (goal 24):
  411. * a markdown document summarising the project's structure and
  412. * anything unusual worth knowing, stamped with the commit hash it
  413. * was written against. Rows are immutable — a refresh INSERTS a
  414. * new revision — and the project's "current" understanding is its
  415. * newest row (see {@link ClankerSaddle.Services.UnderstandingService}).
  416. * Stored in the `project_understandings` table.
  417. */
  418. public class ProjectUnderstanding : Object {
  419. public int64 id { get; set; }
  420. public int64 project_id { get; set; }
  421. public string body { get; set; default = ""; }
  422. /** The commit the document was written against ("" when unknown). */
  423. public string commit_hash { get; set; default = ""; }
  424. /** The understanding task that wrote the row (0 = human/unknown). */
  425. public int64 task_id { get; set; default = 0; }
  426. public DateTime? created_at { get; set; default = null; }
  427. public static void entity_mapping(EntityMapperBuilder<ProjectUnderstanding> cfg) {
  428. cfg.table("project_understandings")
  429. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  430. .column<int64?>("project_id", o => o.project_id, (o, v) => o.project_id = v)
  431. .column<string>("body", o => o.body, (o, v) => o.body = v)
  432. .column<string>("commit_hash", o => o.commit_hash, (o, v) => o.commit_hash = v)
  433. .column<int64?>("task_id", o => (int64?) o.task_id,
  434. (o, v) => o.task_id = v ?? 0)
  435. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  436. }
  437. }
  438. /**
  439. * One row of the discoveries ledger (goal 24): the fleet's global,
  440. * EDITABLE knowledge base of non-project facts — compiler quirks,
  441. * library behaviours, tool flags, protocol gotchas. Unlike the
  442. * immutable Understanding history, a discovery is LIVING: agents
  443. * refine it in place through the update callback (the body is
  444. * replaced, {@link updated_at} bumped) and humans edit or delete
  445. * rows from the Discoveries page, so nuance accumulates in one
  446. * note instead of stacking near-duplicates. Stored in the
  447. * `discoveries` table.
  448. */
  449. public class Discovery : Object {
  450. public int64 id { get; set; }
  451. public string body { get; set; default = ""; }
  452. /** The task whose agent wrote the row (0 = human/unknown). */
  453. public int64 source_task_id { get; set; default = 0; }
  454. /** When the row was first written — frozen. */
  455. public DateTime? created_at { get; set; default = null; }
  456. /** When the body was last refined (equals created_at until then). */
  457. public DateTime? updated_at { get; set; default = null; }
  458. public static void entity_mapping(EntityMapperBuilder<Discovery> cfg) {
  459. cfg.table("discoveries")
  460. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  461. .column<string>("body", o => o.body, (o, v) => o.body = v)
  462. .column<int64?>("source_task_id", o => (int64?) o.source_task_id,
  463. (o, v) => o.source_task_id = v ?? 0)
  464. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v)
  465. .column<DateTime?>("updated_at", o => o.updated_at, (o, v) => o.updated_at = v);
  466. }
  467. }
  468. /**
  469. * A dashboard scratchpad note (goal 8 task 3): a personal
  470. * todo/idea jotted down before a batch of changes is ready to be
  471. * a full goal. Promoting ({@link ClankerSaddle.Services.NoteService.promote_to_goal})
  472. * creates a DRAFT goal from the body and sets {@link goal_id},
  473. * which links the created goal and freezes the note. Stored in
  474. * the `notes` table.
  475. */
  476. public class Note : Object {
  477. public int64 id { get; set; }
  478. public string body { get; set; default = ""; }
  479. public bool done { get; set; default = false; }
  480. public DateTime? done_at { get; set; default = null; }
  481. /** The goal this note promoted into (0 = not promoted). */
  482. public int64 goal_id { get; set; default = 0; }
  483. public DateTime? created_at { get; set; default = null; }
  484. public static void entity_mapping(EntityMapperBuilder<Note> cfg) {
  485. cfg.table("notes")
  486. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  487. .column<string>("body", o => o.body, (o, v) => o.body = v)
  488. .column<bool>("done", o => o.done, (o, v) => o.done = v)
  489. .column<DateTime?>("done_at", o => o.done_at, (o, v) => o.done_at = v)
  490. .column<int64?>("goal_id", o => o.goal_id > 0 ? (int64?) o.goal_id : (int64?) null,
  491. (o, v) => o.goal_id = v ?? 0)
  492. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  493. }
  494. }
  495. /**
  496. * A named tag used for dispatch routing (`capabilities`): saddles
  497. * declare the tags they carry, tasks the tags they require.
  498. */
  499. public class Capability : Object {
  500. public int64 id { get; set; }
  501. public string name { get; set; default = ""; }
  502. public DateTime? created_at { get; set; default = null; }
  503. public static void entity_mapping(EntityMapperBuilder<Capability> cfg) {
  504. cfg.table("capabilities")
  505. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  506. .column<string>("name", o => o.name, (o, v) => o.name = v)
  507. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  508. }
  509. }
  510. /** A tag declaration on a saddle (`saddle_capabilities`). */
  511. public class SaddleCapability : Object {
  512. public int64 id { get; set; }
  513. public int64 capability_id { get; set; }
  514. public int64 saddle_id { get; set; }
  515. public static void entity_mapping(EntityMapperBuilder<SaddleCapability> cfg) {
  516. cfg.table("saddle_capabilities")
  517. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  518. .column<int64?>("capability_id", o => o.capability_id, (o, v) => o.capability_id = v)
  519. .column<int64?>("saddle_id", o => o.saddle_id, (o, v) => o.saddle_id = v);
  520. }
  521. }
  522. /** A required tag on a task (`task_capabilities`). */
  523. public class TaskCapability : Object {
  524. public int64 id { get; set; }
  525. public int64 capability_id { get; set; }
  526. public int64 task_id { get; set; }
  527. public static void entity_mapping(EntityMapperBuilder<TaskCapability> cfg) {
  528. cfg.table("task_capabilities")
  529. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  530. .column<int64?>("capability_id", o => o.capability_id, (o, v) => o.capability_id = v)
  531. .column<int64?>("task_id", o => o.task_id, (o, v) => o.task_id = v);
  532. }
  533. }
  534. /**
  535. * A user's per-task "Notify me" opt-in: the task's first start,
  536. * failure and completion notify the opted-in
  537. * user; block events notify all push-subscribed users globally.
  538. */
  539. public class TaskNotifyPref : Object {
  540. public int64 id { get; set; }
  541. public int64 task_id { get; set; }
  542. public int64 user_id { get; set; }
  543. public static void entity_mapping(EntityMapperBuilder<TaskNotifyPref> cfg) {
  544. cfg.table("task_notify_prefs")
  545. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  546. .column<int64?>("task_id", o => o.task_id, (o, v) => o.task_id = v)
  547. .column<int64?>("user_id", o => o.user_id, (o, v) => o.user_id = v);
  548. }
  549. }
  550. /**
  551. * A user-created goal (plan 12): an agent-orchestrated unit of
  552. * work over one primary project plus optional links. The brief
  553. * locks once started; the lifecycle runs draft → planning → plan
  554. * review → active → completion review → complete / failed /
  555. * cancelled, with paused freezing supervision in between. Stored
  556. * in the `goals` table.
  557. */
  558. public class Goal : Object {
  559. public int64 id { get; set; }
  560. public int64 project_id { get; set; }
  561. public string title { get; set; default = ""; }
  562. public string brief { get; set; default = ""; }
  563. public string branch_name { get; set; default = ""; }
  564. public string? base_branch { get; set; default = null; }
  565. public string status { get; set; default = "draft"; }
  566. public int64 priority { get; set; default = 3; }
  567. public string? plan { get; set; default = null; }
  568. public string? completion_report { get; set; default = null; }
  569. public string creator { get; set; default = "human"; }
  570. /**
  571. * Plan 15 C3 (was last_seen_event_id): the timestamp through
  572. * which the goal agent has consumed its tasks' terminal
  573. * transitions — resume_due fires on transitions newer than
  574. * it, and a resume advance it. The narrative itself is
  575. * derived at read time.
  576. */
  577. public DateTime? last_terminal_watermark { get; set; default = null; }
  578. public DateTime? created_at { get; set; default = null; }
  579. public DateTime? started_at { get; set; default = null; }
  580. public DateTime? completed_at { get; set; default = null; }
  581. public DateTime? paused_at { get; set; default = null; }
  582. /**
  583. * Goal 4 task 1 (Force Wake): the human pressed Force wake on
  584. * the goal page — the parked run becomes offerable immediately
  585. * and the resume notice leads with the explicit wake line.
  586. * Consumed (cleared) by the resume that carries it.
  587. */
  588. public bool force_wake { get; set; default = false; }
  589. public static void entity_mapping(EntityMapperBuilder<Goal> cfg) {
  590. cfg.table("goals")
  591. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  592. .column<int64?>("project_id", o => o.project_id, (o, v) => o.project_id = v)
  593. .column<string>("title", o => o.title, (o, v) => o.title = v)
  594. .column<string>("brief", o => o.brief, (o, v) => o.brief = v)
  595. .column<string>("branch_name", o => o.branch_name, (o, v) => o.branch_name = v)
  596. .column<string>("base_branch", o => o.base_branch, (o, v) => o.base_branch = v)
  597. .column<string>("status", o => o.status, (o, v) => o.status = v)
  598. .column<int64?>("priority", o => o.priority, (o, v) => o.priority = v)
  599. .column<string>("plan", o => o.plan, (o, v) => o.plan = v)
  600. .column<string>("completion_report", o => o.completion_report, (o, v) => o.completion_report = v)
  601. .column<string>("creator", o => o.creator, (o, v) => o.creator = v)
  602. .column<DateTime?>("last_terminal_watermark", o => o.last_terminal_watermark, (o, v) => o.last_terminal_watermark = v)
  603. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v)
  604. .column<DateTime?>("started_at", o => o.started_at, (o, v) => o.started_at = v)
  605. .column<DateTime?>("completed_at", o => o.completed_at, (o, v) => o.completed_at = v)
  606. .column<DateTime?>("paused_at", o => o.paused_at, (o, v) => o.paused_at = v)
  607. .column<bool>("force_wake", o => o.force_wake, (o, v) => o.force_wake = v);
  608. }
  609. }
  610. /** A project link on a goal (`goal_links`): co-developed or reference. */
  611. public class GoalLink : Object {
  612. public int64 id { get; set; }
  613. public int64 goal_id { get; set; }
  614. public int64 project_id { get; set; }
  615. public string relationship { get; set; default = "reference"; }
  616. public static void entity_mapping(EntityMapperBuilder<GoalLink> cfg) {
  617. cfg.table("goal_links")
  618. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  619. .column<int64?>("goal_id", o => o.goal_id, (o, v) => o.goal_id = v)
  620. .column<int64?>("project_id", o => o.project_id, (o, v) => o.project_id = v)
  621. .column<string>("relationship", o => o.relationship, (o, v) => o.relationship = v);
  622. }
  623. }
  624. /**
  625. * One advisor chat (goal 27): a private conversation between a
  626. * user and the read-only AI advisor loop. `status` is the loop's
  627. * coarse state — 'idle', 'running' (a turn is in flight) or
  628. * 'awaiting_answer' (the loop parked on an ask_question). Stored
  629. * in the `advisor_chats` table.
  630. */
  631. public class AdvisorChat : Object {
  632. public int64 id { get; set; }
  633. public int64 user_id { get; set; }
  634. public string title { get; set; default = ""; }
  635. public string status { get; set; default = "idle"; }
  636. public DateTime? created_at { get; set; default = null; }
  637. public DateTime? updated_at { get; set; default = null; }
  638. public static void entity_mapping(EntityMapperBuilder<AdvisorChat> cfg) {
  639. cfg.table("advisor_chats")
  640. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  641. .column<int64?>("user_id", o => o.user_id, (o, v) => o.user_id = v)
  642. .column<string>("title", o => o.title, (o, v) => o.title = v)
  643. .column<string>("status", o => o.status, (o, v) => o.status = v)
  644. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v)
  645. .column<DateTime?>("updated_at", o => o.updated_at, (o, v) => o.updated_at = v);
  646. }
  647. }
  648. /**
  649. * One row of an advisor chat's transcript (goal 27) — the
  650. * conversation stored IN THE WIRE FORMAT so the OpenAI messages
  651. * array reconstructs from rows alone on every turn (no separate
  652. * transcript cache; a restart loses nothing). `kind` is 'user',
  653. * 'assistant', 'tool', 'error' (a failed turn's message) or
  654. * 'system' (an advisor-external notice such as a task completion
  655. * callback). A tool row carries the call's name/arguments/result
  656. * in the JSON `payload`; a question is an assistant row whose
  657. * payload holds `{"options": [...]}` and whose `answer` is filled
  658. * when the human replies. Stored in the `advisor_messages` table.
  659. */
  660. public class AdvisorMessage : Object {
  661. public int64 id { get; set; }
  662. public int64 chat_id { get; set; }
  663. public string kind { get; set; default = "user"; }
  664. public string content { get; set; default = ""; }
  665. /** JSON: a tool row's name/arguments/result; a question's options. */
  666. public string? payload { get; set; default = null; }
  667. /** The human's answer when this row is a parked question. */
  668. public string? answer { get; set; default = null; }
  669. public DateTime? answered_at { get; set; default = null; }
  670. public DateTime? created_at { get; set; default = null; }
  671. public static void entity_mapping(EntityMapperBuilder<AdvisorMessage> cfg) {
  672. cfg.table("advisor_messages")
  673. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  674. .column<int64?>("chat_id", o => o.chat_id, (o, v) => o.chat_id = v)
  675. .column<string>("kind", o => o.kind, (o, v) => o.kind = v)
  676. .column<string>("content", o => o.content, (o, v) => o.content = v)
  677. .column<string>("payload", o => o.payload, (o, v) => o.payload = v)
  678. .column<string>("answer", o => o.answer, (o, v) => o.answer = v)
  679. .column<DateTime?>("answered_at", o => o.answered_at, (o, v) => o.answered_at = v)
  680. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
  681. }
  682. }
  683. /**
  684. * One advisor suggestion (goal 27): a task or goal the advisor
  685. * proposed mid-chat. INERT while 'pending' — nothing is created
  686. * until the chat's user approves it in the UI ('approved' /
  687. * 'declined'); an approved row keeps the created
  688. * {@link task_id}/{@link goal_id} so the completion callback finds
  689. * the chat again. `delivered_at` marks the callback delivered.
  690. * `payload` is the JSON the approve action creates from (title,
  691. * mandate/brief, mode, project_id, priority, branch_name…).
  692. * Stored in the `advisor_suggestions` table.
  693. */
  694. public class AdvisorSuggestion : Object {
  695. public int64 id { get; set; }
  696. public int64 chat_id { get; set; }
  697. public int64 user_id { get; set; }
  698. /** "task" or "goal". */
  699. public string kind { get; set; default = "task"; }
  700. public string payload { get; set; default = "{}"; }
  701. public string status { get; set; default = "pending"; }
  702. public int64 task_id { get; set; default = 0; }
  703. public int64 goal_id { get; set; default = 0; }
  704. public DateTime? created_at { get; set; default = null; }
  705. public DateTime? decided_at { get; set; default = null; }
  706. public DateTime? delivered_at { get; set; default = null; }
  707. public static void entity_mapping(EntityMapperBuilder<AdvisorSuggestion> cfg) {
  708. cfg.table("advisor_suggestions")
  709. .column<int64?>("id", o => o.id, (o, v) => o.id = v)
  710. .column<int64?>("chat_id", o => o.chat_id, (o, v) => o.chat_id = v)
  711. .column<int64?>("user_id", o => o.user_id, (o, v) => o.user_id = v)
  712. .column<string>("kind", o => o.kind, (o, v) => o.kind = v)
  713. .column<string>("payload", o => o.payload, (o, v) => o.payload = v)
  714. .column<string>("status", o => o.status, (o, v) => o.status = v)
  715. .column<int64?>("task_id", o => (int64?) o.task_id,
  716. (o, v) => o.task_id = v ?? 0)
  717. .column<int64?>("goal_id", o => (int64?) o.goal_id,
  718. (o, v) => o.goal_id = v ?? 0)
  719. .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v)
  720. .column<DateTime?>("decided_at", o => o.decided_at, (o, v) => o.decided_at = v)
  721. .column<DateTime?>("delivered_at", o => o.delivered_at, (o, v) => o.delivered_at = v);
  722. }
  723. }
  724. /**
  725. * One per-user preference row (goal 41 task 2): the account
  726. * settings' key/value store — the theme picker's "theme" (a Spry
  727. * theme slug, "" = the core default) and "mode" (auto|light|dark)
  728. * are the first keys; new preferences need no migration. Stored in
  729. * the `user_preferences` table with a COMPOSITE primary key
  730. * (user_id, key), so rows upsert through {@link
  731. * Services.UserPreferenceService}'s raw SQL — the ORM's
  732. * update_async keys on the (introspected, single) primary-key
  733. * column and must not touch this table.
  734. *
  735. * `user_id` references the Spry users table by value (no FK — the
  736. * auth schema owns that table, the same posture as the Spry-owned
  737. * push subscription store).
  738. */
  739. public class UserPreference : Object {
  740. public int64 user_id { get; set; }
  741. public string key { get; set; default = ""; }
  742. public string value { get; set; default = ""; }
  743. public static void entity_mapping(EntityMapperBuilder<UserPreference> cfg) {
  744. cfg.table("user_preferences")
  745. .column<int64?>("user_id", o => o.user_id, (o, v) => o.user_id = v)
  746. .column<string>("key", o => o.key, (o, v) => o.key = v)
  747. .column<string>("value", o => o.value, (o, v) => o.value = v);
  748. }
  749. }
  750. /**
  751. * The deployment's model configuration (the Admin → Model page):
  752. * the OpenAI-compatible endpoint every saddle assignment is sent
  753. * to, plus OPTIONAL advisor-specific overrides (empty falls back
  754. * to the main fields). The SINGLETON row (id 1) of the
  755. * `model_config` table — created by M0010 and seeded ONCE from
  756. * the legacy SADDLE_LLM_ and ADVISOR_LLM_-prefixed environment of
  757. * a pre-page deployment by {@link Services.ModelConfigService};
  758. * afterwards the database is the only source (environment changes
  759. * are ignored).
  760. */
  761. public class ModelConfig : Object {
  762. public int64 id { get; set; default = 1; }
  763. /** The saddle assignments' endpoint base URL ("" = dispatch paused). */
  764. public string base_url { get; set; default = ""; }
  765. /** Its API key ("" = none; some proxies run keyless). */
  766. public string api_key { get; set; default = ""; }
  767. /** Its model name ("" = the provider default). */
  768. public string model { get; set; default = ""; }
  769. /** The advisor's override endpoint ("" = fall back to the main fields). */
  770. public string advisor_base_url { get; set; default = ""; }
  771. /** The advisor's override API key ("" = fall back). */
  772. public string advisor_api_key { get; set; default = ""; }
  773. /** The advisor's override model ("" = fall back). */
  774. public string advisor_model { get; set; default = ""; }
  775. public DateTime? updated_at { get; set; default = null; }
  776. public static void entity_mapping(EntityMapperBuilder<ModelConfig> cfg) {
  777. cfg.table("model_config")
  778. .column<int64?>("id", o => o.id, (o, v) => o.id = v ?? 1)
  779. .column<string>("base_url", o => o.base_url, (o, v) => o.base_url = v)
  780. .column<string>("api_key", o => o.api_key, (o, v) => o.api_key = v)
  781. .column<string>("model", o => o.model, (o, v) => o.model = v)
  782. .column<string>("advisor_base_url", o => o.advisor_base_url, (o, v) => o.advisor_base_url = v)
  783. .column<string>("advisor_api_key", o => o.advisor_api_key, (o, v) => o.advisor_api_key = v)
  784. .column<string>("advisor_model", o => o.advisor_model, (o, v) => o.advisor_model = v)
  785. .column<DateTime?>("updated_at", o => o.updated_at, (o, v) => o.updated_at = v);
  786. }
  787. }
  788. }