@teamscala/service-boot 0.23.316 → 0.23.318
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
|
@@ -157,11 +157,18 @@ export declare function createTaskWithBriefComment(prisma: Prisma, input: {
|
|
|
157
157
|
* implement`). "Is the caller the assignee" becomes a simple equality on the
|
|
158
158
|
* session.
|
|
159
159
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
160
|
+
* RE-BINDS a task another session already owns. Migration 109
|
|
161
|
+
* (`work_items_one_active_task_per_lane`) makes `(kind_data->'lane'->>'pane')`
|
|
162
|
+
* unique WHERE active, so when a lane is re-spawned on a pane whose prior task is
|
|
163
|
+
* still active under a STALE session (the lane died, the row survived),
|
|
164
|
+
* `resolveLaneTask(newSession)` finds nothing and `create` P2002s — measured
|
|
165
|
+
* fleet-wide 2026-08-01: every real lane's bindLane 500'd on this constraint.
|
|
166
|
+
* Refusing to steal (the original design) routes every re-spawn into that
|
|
167
|
+
* dead-end. An assignment task belongs to whichever lane was dispatched to it,
|
|
168
|
+
* and a stale session is by definition dead, so re-binding to the fresh session
|
|
169
|
+
* is the correct recovery: the pane's unique slot moves to the live lane and
|
|
170
|
+
* the brief follows. Returns null only when `maybeTaskId` is absent or not a
|
|
171
|
+
* task — caller falls through to create for an exploratory lane (no workItemId).
|
|
165
172
|
*/
|
|
166
173
|
export declare function adoptAssignmentTask(prisma: Prisma, maybeTaskId: string | null | undefined, lane: {
|
|
167
174
|
session: string;
|
|
@@ -187,6 +187,7 @@ describe("createTaskWithBriefComment (atomic task + first comment = brief)", ()
|
|
|
187
187
|
describe("adoptAssignmentTask (SessionStart adopts the pre-created task + ownership)", () => {
|
|
188
188
|
function fakeAdoptPrisma(existing: Record<string, unknown> | null) {
|
|
189
189
|
const updates: Array<{ id: string } & Record<string, unknown>> = [];
|
|
190
|
+
const updateManyCalls: unknown[] = [];
|
|
190
191
|
const prisma = {
|
|
191
192
|
work_items: {
|
|
192
193
|
findFirst: async () => existing,
|
|
@@ -195,9 +196,13 @@ describe("adoptAssignmentTask (SessionStart adopts the pre-created task + owners
|
|
|
195
196
|
updates.push(row);
|
|
196
197
|
return row;
|
|
197
198
|
},
|
|
199
|
+
updateMany: async (a: unknown) => {
|
|
200
|
+
updateManyCalls.push(a);
|
|
201
|
+
return { count: 0 };
|
|
202
|
+
},
|
|
198
203
|
},
|
|
199
204
|
};
|
|
200
|
-
return { prisma, updates };
|
|
205
|
+
return { prisma, updates, updateManyCalls };
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
it("writes the lane session onto the pre-created task AND sets assignee_id (ownership)", async () => {
|
|
@@ -224,12 +229,22 @@ describe("adoptAssignmentTask (SessionStart adopts the pre-created task + owners
|
|
|
224
229
|
expect(kd.lane.session).toBe("s");
|
|
225
230
|
});
|
|
226
231
|
|
|
227
|
-
it("
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
it("RE-BINDS a task a stale session owns to the fresh lane (pane slot recovery)", async () => {
|
|
233
|
+
// Migration 109 makes (kind_data->'lane'->>'pane') unique WHERE active, so a
|
|
234
|
+
// re-spawn on a pane whose prior task is still active under a dead session
|
|
235
|
+
// cannot create a new task (P2002). adopt re-binds the assignment task to
|
|
236
|
+
// the fresh session + releases the stale pane slot first. A stale session
|
|
237
|
+
// is by definition dead, so the task belongs to whichever lane was
|
|
238
|
+
// dispatched to it — re-binding is correct recovery, not theft.
|
|
239
|
+
const task = { id: "t", kind: "task", kind_data: { lane: { session: "someone-else", pane: "tmux:%9" } } };
|
|
240
|
+
const { prisma, updates, updateManyCalls } = fakeAdoptPrisma(task);
|
|
241
|
+
const out = await adoptAssignmentTask(prisma as never, "t", { session: "fresh", pane: "tmux:%9" });
|
|
242
|
+
expect(out).not.toBeNull();
|
|
243
|
+
expect(updates).toHaveLength(1);
|
|
244
|
+
expect((updates[0].kind_data as { lane: { session: string } }).lane.session).toBe("fresh");
|
|
245
|
+
expect(updates[0].assignee_id).toBe("fresh");
|
|
246
|
+
// The stale pane slot was released before the re-bind (STEAL-before-CLAIM).
|
|
247
|
+
expect(updateManyCalls).toHaveLength(1);
|
|
233
248
|
});
|
|
234
249
|
|
|
235
250
|
it("re-adopts a task already bound to THIS session (idempotent re-bind)", async () => {
|
|
@@ -215,11 +215,18 @@ export async function createTaskWithBriefComment(
|
|
|
215
215
|
* implement`). "Is the caller the assignee" becomes a simple equality on the
|
|
216
216
|
* session.
|
|
217
217
|
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
218
|
+
* RE-BINDS a task another session already owns. Migration 109
|
|
219
|
+
* (`work_items_one_active_task_per_lane`) makes `(kind_data->'lane'->>'pane')`
|
|
220
|
+
* unique WHERE active, so when a lane is re-spawned on a pane whose prior task is
|
|
221
|
+
* still active under a STALE session (the lane died, the row survived),
|
|
222
|
+
* `resolveLaneTask(newSession)` finds nothing and `create` P2002s — measured
|
|
223
|
+
* fleet-wide 2026-08-01: every real lane's bindLane 500'd on this constraint.
|
|
224
|
+
* Refusing to steal (the original design) routes every re-spawn into that
|
|
225
|
+
* dead-end. An assignment task belongs to whichever lane was dispatched to it,
|
|
226
|
+
* and a stale session is by definition dead, so re-binding to the fresh session
|
|
227
|
+
* is the correct recovery: the pane's unique slot moves to the live lane and
|
|
228
|
+
* the brief follows. Returns null only when `maybeTaskId` is absent or not a
|
|
229
|
+
* task — caller falls through to create for an exploratory lane (no workItemId).
|
|
223
230
|
*/
|
|
224
231
|
export async function adoptAssignmentTask(
|
|
225
232
|
prisma: Prisma,
|
|
@@ -234,10 +241,6 @@ export async function adoptAssignmentTask(
|
|
|
234
241
|
// (which a spawn also carries as workItemId) must never be adopted, only
|
|
235
242
|
// created-under. Verified, not assumed (`a-component-may-not-report-a-state...`).
|
|
236
243
|
if (row.kind !== "task") return null;
|
|
237
|
-
const existingSession = (
|
|
238
|
-
(row.kind_data as { lane?: { session?: string } } | null) ?? {}
|
|
239
|
-
).lane?.session;
|
|
240
|
-
if (existingSession && existingSession !== lane.session) return null;
|
|
241
244
|
const laneData = {
|
|
242
245
|
pane: lane.pane,
|
|
243
246
|
session: lane.session,
|
|
@@ -249,6 +252,37 @@ export async function adoptAssignmentTask(
|
|
|
249
252
|
lane: laneData,
|
|
250
253
|
assignee_type: "agent",
|
|
251
254
|
};
|
|
255
|
+
// RELEASE any OTHER active task holding this pane's unique lane slot before
|
|
256
|
+
// writing — migration 109's unique index would otherwise P2002 on the update
|
|
257
|
+
// when a stale task (prior session, same pane) is still active. Same logic
|
|
258
|
+
// as the run-link STEAL-before-CLAIM: a pane's lane slot is singly-owned, so
|
|
259
|
+
// every other row holding it is this lane's own stale prior (or a duplicate the
|
|
260
|
+
// race created) and is released to null-pane so this update can land. The
|
|
261
|
+
// assignment task itself is then re-bound to the fresh session.
|
|
262
|
+
try {
|
|
263
|
+
if (lane.pane) {
|
|
264
|
+
const others = (
|
|
265
|
+
prisma as unknown as {
|
|
266
|
+
work_items?: {
|
|
267
|
+
updateMany: (a: unknown) => Promise<{ count: number }>;
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
).work_items;
|
|
271
|
+
await others?.updateMany({
|
|
272
|
+
where: {
|
|
273
|
+
kind: "task",
|
|
274
|
+
status: "active",
|
|
275
|
+
id: { not: row.id },
|
|
276
|
+
kind_data: { path: ["lane", "pane"], equals: lane.pane },
|
|
277
|
+
},
|
|
278
|
+
data: { kind_data: { lane: { pane: null } } },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// Best-effort: a release failure does not block the adopt — the update below
|
|
283
|
+
// will throw its own P2002 and the handler falls through, converging on the
|
|
284
|
+
// next bind. Logged by the caller's run-link region, not here.
|
|
285
|
+
}
|
|
252
286
|
try {
|
|
253
287
|
return await prisma.work_items.update({
|
|
254
288
|
where: { id: row.id },
|
|
@@ -426,11 +426,6 @@ export const taskSyncRouter = {
|
|
|
426
426
|
const ctx = context as Context;
|
|
427
427
|
const prisma = ctx.prisma as Prisma;
|
|
428
428
|
const log = getAppLogger();
|
|
429
|
-
// DIAG: capture the real exception+stack for the pane-with-run 500
|
|
430
|
-
// (oRPC swallows it to a generic 500; the run-link catch is not firing).
|
|
431
|
-
const __diagPane = (input as { lane?: { pane?: string } })?.lane?.pane ?? ctx.headers?.get("x-lane-pane");
|
|
432
|
-
const __diagSession = (input as { lane?: { session?: string } })?.lane?.session ?? ctx.headers?.get("x-lane-session");
|
|
433
|
-
try {
|
|
434
429
|
// The SessionStart hook sends the pane + the CLI's native session id.
|
|
435
430
|
// SESSION is the match key (kind_data.lane.session) — it is unique per
|
|
436
431
|
// CLI session and re-minted each session, so a fresh session cannot find
|
|
@@ -604,14 +599,6 @@ export const taskSyncRouter = {
|
|
|
604
599
|
});
|
|
605
600
|
}
|
|
606
601
|
return { task_id: taskId, created };
|
|
607
|
-
} catch (__diagErr) {
|
|
608
|
-
log.error("[task-sync] bindLane THREW (diag)", {
|
|
609
|
-
pane: __diagPane, session: __diagSession,
|
|
610
|
-
error: __diagErr instanceof Error ? __diagErr.message : String(__diagErr),
|
|
611
|
-
stack: __diagErr instanceof Error ? __diagErr.stack : undefined,
|
|
612
|
-
});
|
|
613
|
-
throw __diagErr;
|
|
614
|
-
}
|
|
615
602
|
}),
|
|
616
603
|
|
|
617
604
|
/**
|