@teamscala/service-boot 0.23.296 → 0.23.298

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/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.296",
9
+ "version": "0.23.298",
10
10
  "sideEffects": false,
11
11
  "files": [
12
12
  "src"
@@ -393,6 +393,14 @@
393
393
  "types": "./src/fn-routers/task-sync-dispatch.d.ts",
394
394
  "default": "./src/fn-routers/task-sync-dispatch.ts"
395
395
  },
396
+ "./fn-routers/task-sync-lane": {
397
+ "types": "./src/fn-routers/task-sync-lane.d.ts",
398
+ "default": "./src/fn-routers/task-sync-lane.ts"
399
+ },
400
+ "./fn-routers/task-sync-lane.ts": {
401
+ "types": "./src/fn-routers/task-sync-lane.d.ts",
402
+ "default": "./src/fn-routers/task-sync-lane.ts"
403
+ },
396
404
  "./fn-routers/task-sync-projection": {
397
405
  "types": "./src/fn-routers/task-sync-projection.d.ts",
398
406
  "default": "./src/fn-routers/task-sync-projection.ts"
@@ -61,10 +61,15 @@ describe("discardApplied (observable no-task-bound discards)", () => {
61
61
  });
62
62
 
63
63
  describe("lane task binding is deterministic and race-proof (source contract)", () => {
64
+ // Two sources since the router hit its 1000-line budget: lane RESOLUTION
65
+ // moved to task-sync-lane.ts, the bindLane HANDLER stayed on the router. A
66
+ // source-text contract has to point at wherever the code actually lives, so
67
+ // these read separately rather than sharing one handle.
68
+ const laneSrc = Bun.file(new URL("./task-sync-lane.ts", import.meta.url).pathname);
64
69
  const src = Bun.file(new URL("./task-sync-router.ts", import.meta.url).pathname);
65
70
 
66
71
  test("resolveLaneTask orders, so duplicates cannot flip the answer", async () => {
67
- const text = await src.text();
72
+ const text = await laneSrc.text();
68
73
  // An unordered findFirst returns whichever row Postgres hands back. With
69
74
  // two parents for one lane, one turn wrote the lane's todos under parent A
70
75
  // and the next read parent B and found nothing — the list appeared to
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Lane RESOLUTION for task-sync: given a session or a pane, which work_items
7
+ * task IS this lane, and what is it called.
8
+ *
9
+ * Split out of task-sync-router.ts, which had reached 998 of its 1000-line
10
+ * budget — two lines from frozen. That is not tidiness: the budget is a commit
11
+ * gate, so at 998 lines ANY change to task-sync was blocked for every lane, and
12
+ * the four-line fix that needed to land could not. The router header records
13
+ * being split once before for exactly this reason; it grew back.
14
+ *
15
+ * The seam matches the one that split the projection. Everything here answers
16
+ * "which lane, called what" from an identity, and none of it touches the ORPC
17
+ * surface or decides what a task list should become — which is why these move
18
+ * together rather than one at a time: resolveLaneTask alone has seven callers.
19
+ *
20
+ * CAPABILITY HARVEST (capability-harvest-on-split): APP-LOCAL, not a capability
21
+ * candidate. Checked against the 14 platform singletons and the package
22
+ * catalogue; the doctrine test fails plainly, because every export is bound to
23
+ * the work_items schema and to task-sync own kind_data.lane convention, so no
24
+ * unrelated app could install this unmodified.
25
+ */
26
+ import type { Prisma, WorkItem } from "./task-sync-router.ts";
27
+ export declare function resolveLaneWorkName(prisma: Prisma, pane: string): Promise<string | null>;
28
+ export declare function resolveLaneTask(prisma: Prisma, session: string): Promise<WorkItem | null>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Lane RESOLUTION for task-sync: given a session or a pane, which work_items
7
+ * task IS this lane, and what is it called.
8
+ *
9
+ * Split out of task-sync-router.ts, which had reached 998 of its 1000-line
10
+ * budget — two lines from frozen. That is not tidiness: the budget is a commit
11
+ * gate, so at 998 lines ANY change to task-sync was blocked for every lane, and
12
+ * the four-line fix that needed to land could not. The router header records
13
+ * being split once before for exactly this reason; it grew back.
14
+ *
15
+ * The seam matches the one that split the projection. Everything here answers
16
+ * "which lane, called what" from an identity, and none of it touches the ORPC
17
+ * surface or decides what a task list should become — which is why these move
18
+ * together rather than one at a time: resolveLaneTask alone has seven callers.
19
+ *
20
+ * CAPABILITY HARVEST (capability-harvest-on-split): APP-LOCAL, not a capability
21
+ * candidate. Checked against the 14 platform singletons and the package
22
+ * catalogue; the doctrine test fails plainly, because every export is bound to
23
+ * the work_items schema and to task-sync own kind_data.lane convention, so no
24
+ * unrelated app could install this unmodified.
25
+ */
26
+
27
+ import type { Prisma, WorkItem } from "./task-sync-router.ts";
28
+
29
+ export async function resolveLaneWorkName(prisma: Prisma, pane: string): Promise<string | null> {
30
+ try {
31
+ const runs = (
32
+ prisma as unknown as {
33
+ fleet_agent_runs?: {
34
+ findFirst: (a: unknown) => Promise<{ slot_name?: string | null } | null>;
35
+ };
36
+ }
37
+ ).fleet_agent_runs;
38
+ if (!runs?.findFirst) return null;
39
+ const run = await runs.findFirst({
40
+ where: { pane, state: { not: "closed" } },
41
+ select: { slot_name: true },
42
+ });
43
+ const name = run?.slot_name?.trim();
44
+ return name ? name : null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ export async function resolveLaneTask(prisma: Prisma, session: string): Promise<WorkItem | null> {
51
+ const active = await prisma.work_items.findFirst({
52
+ where: {
53
+ kind: "task",
54
+ status: "active",
55
+ kind_data: { path: ["lane", "session"], equals: session },
56
+ },
57
+ // Deterministic by oldest. An unordered findFirst returns whichever row
58
+ // Postgres happens to hand back, so with two parents for one lane (the
59
+ // bindLane race, below) one turn wrote the lane's todos under parent A
60
+ // and the next read parent B and found nothing — the lane's task list
61
+ // appeared to empty itself, and the watchdog accused lanes that were
62
+ // holding a populated list. Migration 109 makes the duplicate
63
+ // unwritable; this makes the read stable regardless.
64
+ orderBy: { created_at: "asc" },
65
+ });
66
+ if (active) return active;
67
+
68
+ // FALL BACK TO AN ARCHIVED PARENT FOR THE SAME SESSION.
69
+ //
70
+ // Closing a run archives the lane's parent (close-run.ts) specifically to
71
+ // drop it from the active match set above. That is correct when the LANE is
72
+ // gone, and wrong whenever the run record closes while the lane keeps
73
+ // running — which happens on a tmux-server restart (runs are reaped, panes
74
+ // are restored) and whenever an orchestrator closes a run on a stale read.
75
+ // The lane then has a full task list that this function cannot see, so
76
+ // callers mint a FRESH parent, the old list is orphaned, and the compliance
77
+ // watchdog reports "working with NO task list" at a lane holding 85 tasks.
78
+ // That is the "KNOWN ADJACENT DEFECT" recorded on the lane-working-has-task-list
79
+ // guard row, observed live 2026-07-31.
80
+ //
81
+ // Safe because a session id is minted per lane spawn and never reused, so an
82
+ // archived parent under this session belongs to THIS lane. Newest-first.
83
+ return prisma.work_items.findFirst({
84
+ where: {
85
+ kind: "task",
86
+ status: "archived",
87
+ kind_data: { path: ["lane", "session"], equals: session },
88
+ },
89
+ orderBy: { created_at: "desc" },
90
+ });
91
+ }
@@ -92,6 +92,17 @@ export interface WorkItem {
92
92
  completed: boolean | null;
93
93
  kind_data: Record<string, unknown>;
94
94
  }
95
+ export type Prisma = {
96
+ work_items: {
97
+ findFirst: (a: unknown) => Promise<WorkItem | null>;
98
+ findMany: (a: unknown) => Promise<WorkItem[]>;
99
+ create: (a: unknown) => Promise<WorkItem>;
100
+ update: (a: unknown) => Promise<WorkItem>;
101
+ deleteMany: (a: unknown) => Promise<{
102
+ count: number;
103
+ }>;
104
+ };
105
+ };
95
106
  /**
96
107
  * A dispatched run already points at its fleet_tasks row. Task-sync may fill
97
108
  * the convenience join for an adopted lane, but must never replace that
@@ -40,6 +40,7 @@ import { recordEvent } from "@teamscala/event-log/record-event";
40
40
  import { getPlatformOrgId } from "@teamscala/db/registry/platform";
41
41
  import { authed, type Context } from "./auth-router.ts";
42
42
  import { notifyLaneOfTaskChange } from "./task-sync-dispatch.ts";
43
+ import { resolveLaneTask, resolveLaneWorkName } from "./task-sync-lane.ts";
43
44
  import * as v from "valibot";
44
45
  import {
45
46
  STATUS_TO_WORK_ITEM,
@@ -113,7 +114,7 @@ export interface WorkItem {
113
114
  kind_data: Record<string, unknown>;
114
115
  }
115
116
 
116
- type Prisma = {
117
+ export type Prisma = {
117
118
  work_items: {
118
119
  findFirst: (a: unknown) => Promise<WorkItem | null>;
119
120
  findMany: (a: unknown) => Promise<WorkItem[]>;
@@ -189,26 +190,6 @@ export function discardApplied(
189
190
  * name must not block the bind. Feature-detected like the run-link below, because
190
191
  * fleet_agent_runs lives only in the fleet service's schema.
191
192
  */
192
- async function resolveLaneWorkName(prisma: Prisma, pane: string): Promise<string | null> {
193
- try {
194
- const runs = (
195
- prisma as unknown as {
196
- fleet_agent_runs?: {
197
- findFirst: (a: unknown) => Promise<{ slot_name?: string | null } | null>;
198
- };
199
- }
200
- ).fleet_agent_runs;
201
- if (!runs?.findFirst) return null;
202
- const run = await runs.findFirst({
203
- where: { pane, state: { not: "closed" } },
204
- select: { slot_name: true },
205
- });
206
- const name = run?.slot_name?.trim();
207
- return name ? name : null;
208
- } catch {
209
- return null;
210
- }
211
- }
212
193
 
213
194
  /**
214
195
  * Resolve a lane's CLI-native session id from its live run row by pane. Used by
@@ -289,48 +270,6 @@ async function ensureLaneTask(
289
270
  }
290
271
  }
291
272
 
292
- async function resolveLaneTask(prisma: Prisma, session: string): Promise<WorkItem | null> {
293
- const active = await prisma.work_items.findFirst({
294
- where: {
295
- kind: "task",
296
- status: "active",
297
- kind_data: { path: ["lane", "session"], equals: session },
298
- },
299
- // Deterministic by oldest. An unordered findFirst returns whichever row
300
- // Postgres happens to hand back, so with two parents for one lane (the
301
- // bindLane race, below) one turn wrote the lane's todos under parent A
302
- // and the next read parent B and found nothing — the lane's task list
303
- // appeared to empty itself, and the watchdog accused lanes that were
304
- // holding a populated list. Migration 109 makes the duplicate
305
- // unwritable; this makes the read stable regardless.
306
- orderBy: { created_at: "asc" },
307
- });
308
- if (active) return active;
309
-
310
- // FALL BACK TO AN ARCHIVED PARENT FOR THE SAME SESSION.
311
- //
312
- // Closing a run archives the lane's parent (close-run.ts) specifically to
313
- // drop it from the active match set above. That is correct when the LANE is
314
- // gone, and wrong whenever the run record closes while the lane keeps
315
- // running — which happens on a tmux-server restart (runs are reaped, panes
316
- // are restored) and whenever an orchestrator closes a run on a stale read.
317
- // The lane then has a full task list that this function cannot see, so
318
- // callers mint a FRESH parent, the old list is orphaned, and the compliance
319
- // watchdog reports "working with NO task list" at a lane holding 85 tasks.
320
- // That is the "KNOWN ADJACENT DEFECT" recorded on the lane-working-has-task-list
321
- // guard row, observed live 2026-07-31.
322
- //
323
- // Safe because a session id is minted per lane spawn and never reused, so an
324
- // archived parent under this session belongs to THIS lane. Newest-first.
325
- return prisma.work_items.findFirst({
326
- where: {
327
- kind: "task",
328
- status: "archived",
329
- kind_data: { path: ["lane", "session"], equals: session },
330
- },
331
- orderBy: { created_at: "desc" },
332
- });
333
- }
334
273
 
335
274
  /** Match key for a subtask: stable mark when the CLI provides one, else content, else title. Uses || (not ??) so an empty-string content (passed for existing rows that key by title) falls through. */
336
275
  /**
@@ -518,7 +457,16 @@ export const taskSyncRouter = {
518
457
  const kindData = { ...existing.kind_data, lane };
519
458
  const updated = await prisma.work_items.update({
520
459
  where: { id: existing.id },
521
- data: { kind_data: kindData },
460
+ // A supplied parent is an INSTRUCTION, not a default: the spawn named
461
+ // the project this lane is for, which is newer than whatever the row
462
+ // carried. Set only on create until now, so a re-bind — the common
463
+ // case, since this matches by session — silently dropped it.
464
+ // Omitted leaves the existing parent alone, so a re-bind without a
465
+ // project cannot orphan a lane that already has one.
466
+ data: {
467
+ kind_data: kindData,
468
+ ...(raw.parent_id ? { parent_id: raw.parent_id } : {}),
469
+ },
522
470
  });
523
471
  taskId = updated.id;
524
472
  created = false;
@@ -97,7 +97,10 @@ async function scalaToolsCurrencyWatch(_ctx: { config: ScheduledJobConfig }): Pr
97
97
  );
98
98
  const install = await spawn({
99
99
  name: "scala-tools-currency-watch:deps-install",
100
- command: ["scala-tools", "deps", "install", "--force-resolve"],
100
+ // A plain install fetches the current version, which is the whole point
101
+ // of this watch. Re-resolving the entire tree is an emergency hatch, not
102
+ // a currency step (operator ruling 2026-07-31).
103
+ command: ["scala-tools", "deps", "install"],
101
104
  cwd: SENTINEL_DIR,
102
105
  timeoutMs: 300_000,
103
106
  });
@@ -113,7 +113,11 @@ async function checkCurrency(): Promise<{ healthy: boolean; details?: Record<str
113
113
  });
114
114
  const result = await spawn({
115
115
  name: "scala-tools-currency-repair",
116
- command: ["node_modules/.bin/scala-tools", "deps", "install", "--dir", cwd, "--force-resolve"],
116
+ // A stale installed version needs the current one fetched, which a plain
117
+ // install does. Re-resolving the whole tree to achieve that is an emergency
118
+ // escape hatch, not a currency step, and running it automatically burns host
119
+ // resources for no gain (operator ruling 2026-07-31).
120
+ command: ["node_modules/.bin/scala-tools", "deps", "install", "--dir", cwd],
117
121
  cwd,
118
122
  timeoutMs: cfg.command_timeout_ms,
119
123
  captureStderr: true,