@teamscala/service-boot 0.23.330 → 0.23.331

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.330",
9
+ "version": "0.23.331",
10
10
  "sideEffects": false,
11
11
  "files": [
12
12
  "src"
@@ -402,6 +402,14 @@
402
402
  "types": "./src/fn-routers/task-sync-brief.d.ts",
403
403
  "default": "./src/fn-routers/task-sync-brief.ts"
404
404
  },
405
+ "./fn-routers/task-sync-close": {
406
+ "types": "./src/fn-routers/task-sync-close.d.ts",
407
+ "default": "./src/fn-routers/task-sync-close.ts"
408
+ },
409
+ "./fn-routers/task-sync-close.ts": {
410
+ "types": "./src/fn-routers/task-sync-close.d.ts",
411
+ "default": "./src/fn-routers/task-sync-close.ts"
412
+ },
405
413
  "./fn-routers/task-sync-dispatch": {
406
414
  "types": "./src/fn-routers/task-sync-dispatch.d.ts",
407
415
  "default": "./src/fn-routers/task-sync-dispatch.ts"
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Lane-close write-through for task-sync. The session-owning fleet runtime
7
+ * calls this ORPC procedure before releasing an ACP lane, so work_items remains
8
+ * the durable lifecycle source without a second raw-SQL writer in the host.
9
+ */
10
+ import * as v from "valibot";
11
+ import { type Context } from "./auth-router.ts";
12
+ import type { WorkItem } from "./task-sync-router.ts";
13
+ export declare const closeLaneInput: v.ObjectSchema<{
14
+ readonly lane_session: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "lane_session is required">]>;
15
+ readonly disposition: v.PicklistSchema<["completed", "needs_retry", "failed", "orchestrator_closed", "orphaned"], undefined>;
16
+ }, undefined>;
17
+ export interface ClosedTaskState {
18
+ task_id: string;
19
+ task_status: "done" | "archived";
20
+ completed: boolean;
21
+ kind_data: Record<string, unknown>;
22
+ }
23
+ export declare function closedTaskState(task: Pick<WorkItem, "id" | "kind_data">, disposition: v.InferOutput<typeof closeLaneInput>["disposition"]): ClosedTaskState;
24
+ export declare const closeLaneHandler: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
25
+ headers: Headers;
26
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
27
+ prisma: any;
28
+ user: import("./auth-router.ts").User;
29
+ orgId: string | null | undefined;
30
+ }>, v.ObjectSchema<{
31
+ readonly lane_session: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "lane_session is required">]>;
32
+ readonly disposition: v.PicklistSchema<["completed", "needs_retry", "failed", "orchestrator_closed", "orphaned"], undefined>;
33
+ }, undefined>, import("@orpc/contract").Schema<{
34
+ task_id: string;
35
+ task_status: "archived" | "done";
36
+ reconciled: boolean;
37
+ }, {
38
+ task_id: string;
39
+ task_status: "archived" | "done";
40
+ reconciled: boolean;
41
+ }>, Record<never, never>, Record<never, never>>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ */
6
+
7
+ import { describe, expect, it } from "bun:test";
8
+ import { closedTaskState } from "./task-sync-close.ts";
9
+
10
+ const task = {
11
+ id: "task-1",
12
+ kind_data: {
13
+ lane: { session: "acp-session", cli_kind: "zerostack", live: true },
14
+ assignee_type: "agent",
15
+ },
16
+ };
17
+
18
+ describe("closedTaskState", () => {
19
+ it("marks completed work done and the lane non-live", () => {
20
+ expect(closedTaskState(task, "completed")).toEqual({
21
+ task_id: "task-1",
22
+ task_status: "done",
23
+ completed: true,
24
+ kind_data: {
25
+ lane: { session: "acp-session", cli_kind: "zerostack", live: false },
26
+ assignee_type: "agent",
27
+ },
28
+ });
29
+ });
30
+
31
+ it("archives every disposition that does not prove completion", () => {
32
+ for (const disposition of [
33
+ "needs_retry",
34
+ "failed",
35
+ "orchestrator_closed",
36
+ "orphaned",
37
+ ] as const) {
38
+ const state = closedTaskState(task, disposition);
39
+ expect(state.task_status).toBe("archived");
40
+ expect(state.completed).toBe(false);
41
+ expect((state.kind_data.lane as { live: boolean }).live).toBe(false);
42
+ }
43
+ });
44
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Lane-close write-through for task-sync. The session-owning fleet runtime
7
+ * calls this ORPC procedure before releasing an ACP lane, so work_items remains
8
+ * the durable lifecycle source without a second raw-SQL writer in the host.
9
+ */
10
+
11
+ import { ORPCError } from "@orpc/server";
12
+ import * as v from "valibot";
13
+ import { authed, type Context } from "./auth-router.ts";
14
+ import type { Prisma, WorkItem } from "./task-sync-router.ts";
15
+
16
+ export const closeLaneInput = v.object({
17
+ lane_session: v.pipe(v.string(), v.minLength(1, "lane_session is required")),
18
+ disposition: v.picklist([
19
+ "completed",
20
+ "needs_retry",
21
+ "failed",
22
+ "orchestrator_closed",
23
+ "orphaned",
24
+ ]),
25
+ });
26
+
27
+ export interface ClosedTaskState {
28
+ task_id: string;
29
+ task_status: "done" | "archived";
30
+ completed: boolean;
31
+ kind_data: Record<string, unknown>;
32
+ }
33
+
34
+ export function closedTaskState(
35
+ task: Pick<WorkItem, "id" | "kind_data">,
36
+ disposition: v.InferOutput<typeof closeLaneInput>["disposition"],
37
+ ): ClosedTaskState {
38
+ const completed = disposition === "completed";
39
+ const lane =
40
+ task.kind_data.lane && typeof task.kind_data.lane === "object"
41
+ ? (task.kind_data.lane as Record<string, unknown>)
42
+ : {};
43
+ return {
44
+ task_id: task.id,
45
+ task_status: completed ? "done" : "archived",
46
+ completed,
47
+ kind_data: { ...task.kind_data, lane: { ...lane, live: false } },
48
+ };
49
+ }
50
+
51
+ export const closeLaneHandler = authed
52
+ .input(closeLaneInput)
53
+ .handler(async ({ input, context }) => {
54
+ const prisma = (context as Context).prisma as Prisma;
55
+ const task = await prisma.work_items.findFirst({
56
+ where: {
57
+ kind: "task",
58
+ kind_data: { path: ["lane", "session"], equals: input.lane_session },
59
+ },
60
+ orderBy: { created_at: "desc" },
61
+ });
62
+ if (!task) {
63
+ throw new ORPCError("NOT_FOUND", {
64
+ message: `No task is bound to ACP lane ${input.lane_session}`,
65
+ });
66
+ }
67
+ const state = closedTaskState(task, input.disposition);
68
+ await prisma.work_items.update({
69
+ where: { id: task.id },
70
+ data: {
71
+ kind_data: state.kind_data,
72
+ status: state.task_status,
73
+ completed: state.completed,
74
+ },
75
+ select: { id: true },
76
+ });
77
+ return {
78
+ task_id: state.task_id,
79
+ task_status: state.task_status,
80
+ reconciled: true,
81
+ };
82
+ });
@@ -26,6 +26,7 @@
26
26
  *
27
27
  * These procedures are the ONLY CLI-agnostic surface an adapter calls:
28
28
  * bindLane — bind (create-or-confirm) a lane's task. SessionStart.
29
+ * closeLane — reconcile a session-owned lane before its runtime releases it.
29
30
  * upsertFromTaskList — write a lane's current task list through to subtasks (AGENT→DB leg, OpenCode whole-list).
30
31
  * upsertFromTaskHook — apply a per-item Claude Code PostToolUse:TaskCreate/Update/Delete (AGENT→DB leg, Claude per-item).
31
32
  * readTaskList — read a lane's current subtasks as a normalized list (DB→AGENT backstop).
@@ -344,6 +345,25 @@ export declare const taskSyncRouter: {
344
345
  }, {
345
346
  work_item_id: string;
346
347
  }>, Record<never, never>, Record<never, never>>;
348
+ /** RUNTIME→DB lifecycle close: mark lane.live=false and reconcile task status before the ACP owner releases the session. */
349
+ closeLane: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
350
+ headers: Headers;
351
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
352
+ prisma: any;
353
+ user: import("./auth-router.ts").User;
354
+ orgId: string | null | undefined;
355
+ }>, v.ObjectSchema<{
356
+ readonly lane_session: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "lane_session is required">]>;
357
+ readonly disposition: v.PicklistSchema<["completed", "needs_retry", "failed", "orchestrator_closed", "orphaned"], undefined>;
358
+ }, undefined>, import("@orpc/contract").Schema<{
359
+ task_id: string;
360
+ task_status: "archived" | "done";
361
+ reconciled: boolean;
362
+ }, {
363
+ task_id: string;
364
+ task_status: "archived" | "done";
365
+ reconciled: boolean;
366
+ }>, Record<never, never>, Record<never, never>>;
347
367
  /**
348
368
  * TAB→DB rename back-write: the session-picker `/rpc/rename` calls this after
349
369
  * renaming a tmux window to update the bound task's title BY SESSION (never
@@ -26,6 +26,7 @@
26
26
  *
27
27
  * These procedures are the ONLY CLI-agnostic surface an adapter calls:
28
28
  * bindLane — bind (create-or-confirm) a lane's task. SessionStart.
29
+ * closeLane — reconcile a session-owned lane before its runtime releases it.
29
30
  * upsertFromTaskList — write a lane's current task list through to subtasks (AGENT→DB leg, OpenCode whole-list).
30
31
  * upsertFromTaskHook — apply a per-item Claude Code PostToolUse:TaskCreate/Update/Delete (AGENT→DB leg, Claude per-item).
31
32
  * readTaskList — read a lane's current subtasks as a normalized list (DB→AGENT backstop).
@@ -44,6 +45,7 @@ import { notifyLaneOfTaskChange } from "./task-sync-dispatch.ts";
44
45
  import { resolveLaneTask, resolveLaneWorkName } from "./task-sync-lane.ts";
45
46
  import { readAssignedBriefHandler } from "./task-sync-brief.ts";
46
47
  import { renameLaneTaskHandler } from "./task-sync-rename.ts";
48
+ import { closeLaneHandler } from "./task-sync-close.ts";
47
49
  import {
48
50
  adoptAssignmentTask,
49
51
  bindLaneInput,
@@ -773,6 +775,9 @@ export const taskSyncRouter = {
773
775
  /** SPAWNER→DB brief mint: `spawn_agent_tab` turns an initialPrompt into the task row (brief = first comment) the lane self-briefs from; returns the work_item_id that rides the spawn payload. Handler in task-sync-assign.ts. */
774
776
  createBriefTask: createBriefTaskHandler,
775
777
 
778
+ /** RUNTIME→DB lifecycle close: mark lane.live=false and reconcile task status before the ACP owner releases the session. */
779
+ closeLane: closeLaneHandler,
780
+
776
781
  /**
777
782
  * TAB→DB rename back-write: the session-picker `/rpc/rename` calls this after
778
783
  * renaming a tmux window to update the bound task's title BY SESSION (never