@teamscala/service-boot 0.23.297 → 0.23.299
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 +10 -1
- package/src/fn-routers/task-sync-bind.test.ts +6 -1
- package/src/fn-routers/task-sync-lane.d.ts +28 -0
- package/src/fn-routers/task-sync-lane.ts +91 -0
- package/src/fn-routers/task-sync-router.d.ts +11 -0
- package/src/fn-routers/task-sync-router.ts +12 -64
- package/src/serve/serve.ts +26 -0
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"name": "@teamscala/service-boot",
|
|
8
8
|
"type": "module",
|
|
9
|
-
"version": "0.23.
|
|
9
|
+
"version": "0.23.299",
|
|
10
10
|
"sideEffects": false,
|
|
11
11
|
"files": [
|
|
12
12
|
"src"
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@teamscala/object-storage": "*",
|
|
36
36
|
"@teamscala/orpc": "*",
|
|
37
37
|
"@teamscala/os": "*",
|
|
38
|
+
"@teamscala/proc-walker": "*",
|
|
38
39
|
"@teamscala/realtime-utils": "*",
|
|
39
40
|
"@teamscala/resolver-engine": "*",
|
|
40
41
|
"@teamscala/timing": "*",
|
|
@@ -393,6 +394,14 @@
|
|
|
393
394
|
"types": "./src/fn-routers/task-sync-dispatch.d.ts",
|
|
394
395
|
"default": "./src/fn-routers/task-sync-dispatch.ts"
|
|
395
396
|
},
|
|
397
|
+
"./fn-routers/task-sync-lane": {
|
|
398
|
+
"types": "./src/fn-routers/task-sync-lane.d.ts",
|
|
399
|
+
"default": "./src/fn-routers/task-sync-lane.ts"
|
|
400
|
+
},
|
|
401
|
+
"./fn-routers/task-sync-lane.ts": {
|
|
402
|
+
"types": "./src/fn-routers/task-sync-lane.d.ts",
|
|
403
|
+
"default": "./src/fn-routers/task-sync-lane.ts"
|
|
404
|
+
},
|
|
396
405
|
"./fn-routers/task-sync-projection": {
|
|
397
406
|
"types": "./src/fn-routers/task-sync-projection.d.ts",
|
|
398
407
|
"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
|
|
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
|
-
|
|
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;
|
package/src/serve/serve.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Hono } from "hono";
|
|
12
12
|
import { createBunWebSocket } from "hono/bun";
|
|
13
|
+
import { resolveSocketPeer } from "@teamscala/proc-walker/socket-peer";
|
|
13
14
|
|
|
14
15
|
const _bunWs = createBunWebSocket();
|
|
15
16
|
|
|
@@ -88,6 +89,31 @@ function createFetchHandler(
|
|
|
88
89
|
headers.delete("x-caller-remote-address");
|
|
89
90
|
headers.set("x-caller-remote-address", String(address));
|
|
90
91
|
}
|
|
92
|
+
// Local-caller attribution: a local `fetch` carries no forwarding
|
|
93
|
+
// header, so for a loopback peer (no CF-Connecting-IP / X-Forwarded-
|
|
94
|
+
// For) resolve the owning local PROCESS from the socket + stamp it for
|
|
95
|
+
// the access log. Names the daemon behind every local request —
|
|
96
|
+
// including an unidentified sweeper keeping scale-to-zero apps awake.
|
|
97
|
+
// Trust stays transport-derived (no-client-controlled-trust-signals);
|
|
98
|
+
// the comm is attribution only. resolveSocketPeer is TTL-cached + never
|
|
99
|
+
// throws (returns null on any miss); skipped for external callers.
|
|
100
|
+
if (
|
|
101
|
+
address &&
|
|
102
|
+
(address === "127.0.0.1" ||
|
|
103
|
+
address === "::1" ||
|
|
104
|
+
address === "::ffff:127.0.0.1") &&
|
|
105
|
+
!headers.has("cf-connecting-ip") &&
|
|
106
|
+
!headers.get("x-forwarded-for") &&
|
|
107
|
+
typeof port === "number" &&
|
|
108
|
+
port > 0
|
|
109
|
+
) {
|
|
110
|
+
const owner = resolveSocketPeer(port);
|
|
111
|
+
if (owner) {
|
|
112
|
+
headers.set("x-caller-local-comm", owner.comm);
|
|
113
|
+
const cmd = owner.cmdline.join(" ").trim();
|
|
114
|
+
if (cmd) headers.set("x-caller-local-cmd", cmd.slice(0, 160));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
91
117
|
const cloned = new Request(req.url, {
|
|
92
118
|
method: req.method,
|
|
93
119
|
headers,
|