@teamscala/service-boot 0.23.314 → 0.23.316

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.314",
9
+ "version": "0.23.316",
10
10
  "sideEffects": false,
11
11
  "files": [
12
12
  "src"
@@ -93,16 +93,19 @@ export type AssignTaskInput = v.InferOutput<typeof assignTaskInput>;
93
93
  * "fn.taskSync is 404" and reached for serving changes, when the row was simply
94
94
  * never bound because the body never parsed.
95
95
  *
96
- * With the schema oRPC parses the body, so `parent_id` reaches the adopt path.
97
- * Every field is OPTIONAL and the handler keeps a header fallback for each, so a
98
- * caller that transports everything on headers (zerostack: x-lane-pane/session/
99
- * work-item) parses to an empty object and behaves EXACTLY as before — the schema
100
- * ADDS the body path without removing the header one. As a side effect it also
101
- * fixes zerostack cli_kind mis-attribution: zerostack's body `{lane:{cli_kind:
102
- * "zerostack"}}` was never parsed, so its cli_kind fell through every header to
103
- * the "claude-code" default; parsed now, it reads "zerostack" as intended.
96
+ * ROOT-OPTIONAL (`v.optional(v.object(...))`) so it tolerates a caller that sends
97
+ * a DIRECT body or none at all: oRPC extracts `input` from the `{json:{...}}` RPC
98
+ * envelope, so a raw-object or empty POST yields `undefined`, which the optional
99
+ * root ACCEPTS — the handler then falls back to its headers, exactly as it did
100
+ * schema-less (a header-only caller like zerostack is unchanged). An adapter that
101
+ * wants the body path (pi's `parent_id`) POSTs the `{json:{...}}` envelope and
102
+ * `parent_id` is parsed + reaches `adoptAssignmentTask` — the same envelope
103
+ * convention pi ALREADY uses for upsertFromTaskList. A raw-object POST is NOT
104
+ * parsed (oRPC hands the handler `undefined`), which is why pi's bindLaneTask
105
+ * must move to the envelope to deliver `parent_id`. Every field is optional with
106
+ * a header fallback, so the schema ADDS the body path without removing the header.
104
107
  */
105
- export declare const bindLaneInput: v.ObjectSchema<{
108
+ export declare const bindLaneInput: v.OptionalSchema<v.ObjectSchema<{
106
109
  readonly lane: v.OptionalSchema<v.ObjectSchema<{
107
110
  readonly pane: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
108
111
  readonly session: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
@@ -112,7 +115,7 @@ export declare const bindLaneInput: v.ObjectSchema<{
112
115
  }, undefined>, undefined>;
113
116
  readonly title: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
114
117
  readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
115
- }, undefined>;
118
+ }, undefined>, undefined>;
116
119
  /**
117
120
  * Resolve the org a dispatched task is created under. Pure: the platform org is
118
121
  * INJECTED (resolved by the caller at the handler boundary, where a registry
@@ -309,8 +309,15 @@ describe("bindLaneInput (body parent_id reaches the adopt path — schema-less g
309
309
  // A caller transporting everything on headers sends {} (or omits the
310
310
  // body); the schema must not reject it, and every field is undefined so the
311
311
  // handler's header fallbacks carry the values.
312
- const out = v.safeParse(bindLaneInput, {});
313
- expect(out.success).toBe(true);
312
+ expect(v.safeParse(bindLaneInput, {}).success).toBe(true);
313
+ });
314
+
315
+ it("ACCEPTS undefined input — a raw-object POST yields undefined (root-optional)", () => {
316
+ // oRPC extracts input from the {json:{...}} envelope; a raw-object POST
317
+ // (what zerostack/pi send today) yields undefined. The root MUST be optional
318
+ // or those callers 400 instead of falling back to headers. A strict root
319
+ // regressed exactly this (0.23.314), so this assertion locks the tolerance.
320
+ expect(v.safeParse(bindLaneInput, undefined).success).toBe(true);
314
321
  });
315
322
 
316
323
  it("treats every field as optional (no caller is forced to populate the body)", () => {
@@ -105,28 +105,33 @@ export type AssignTaskInput = v.InferOutput<typeof assignTaskInput>;
105
105
  * "fn.taskSync is 404" and reached for serving changes, when the row was simply
106
106
  * never bound because the body never parsed.
107
107
  *
108
- * With the schema oRPC parses the body, so `parent_id` reaches the adopt path.
109
- * Every field is OPTIONAL and the handler keeps a header fallback for each, so a
110
- * caller that transports everything on headers (zerostack: x-lane-pane/session/
111
- * work-item) parses to an empty object and behaves EXACTLY as before — the schema
112
- * ADDS the body path without removing the header one. As a side effect it also
113
- * fixes zerostack cli_kind mis-attribution: zerostack's body `{lane:{cli_kind:
114
- * "zerostack"}}` was never parsed, so its cli_kind fell through every header to
115
- * the "claude-code" default; parsed now, it reads "zerostack" as intended.
108
+ * ROOT-OPTIONAL (`v.optional(v.object(...))`) so it tolerates a caller that sends
109
+ * a DIRECT body or none at all: oRPC extracts `input` from the `{json:{...}}` RPC
110
+ * envelope, so a raw-object or empty POST yields `undefined`, which the optional
111
+ * root ACCEPTS — the handler then falls back to its headers, exactly as it did
112
+ * schema-less (a header-only caller like zerostack is unchanged). An adapter that
113
+ * wants the body path (pi's `parent_id`) POSTs the `{json:{...}}` envelope and
114
+ * `parent_id` is parsed + reaches `adoptAssignmentTask` — the same envelope
115
+ * convention pi ALREADY uses for upsertFromTaskList. A raw-object POST is NOT
116
+ * parsed (oRPC hands the handler `undefined`), which is why pi's bindLaneTask
117
+ * must move to the envelope to deliver `parent_id`. Every field is optional with
118
+ * a header fallback, so the schema ADDS the body path without removing the header.
116
119
  */
117
- export const bindLaneInput = v.object({
118
- lane: v.optional(
119
- v.object({
120
- pane: v.optional(v.string()),
121
- session: v.optional(v.string()),
122
- cli_kind: v.optional(v.string()),
123
- role: v.optional(v.string()),
124
- organisation_id: v.optional(v.string()),
125
- }),
126
- ),
127
- title: v.optional(v.string()),
128
- parent_id: v.optional(v.string()),
129
- });
120
+ export const bindLaneInput = v.optional(
121
+ v.object({
122
+ lane: v.optional(
123
+ v.object({
124
+ pane: v.optional(v.string()),
125
+ session: v.optional(v.string()),
126
+ cli_kind: v.optional(v.string()),
127
+ role: v.optional(v.string()),
128
+ organisation_id: v.optional(v.string()),
129
+ }),
130
+ ),
131
+ title: v.optional(v.string()),
132
+ parent_id: v.optional(v.string()),
133
+ }),
134
+ );
130
135
 
131
136
  /**
132
137
  * Resolve the org a dispatched task is created under. Pure: the platform org is
@@ -187,7 +187,7 @@ export declare const taskSyncRouter: {
187
187
  prisma: any;
188
188
  user: import("./auth-router.ts").User;
189
189
  orgId: string | null | undefined;
190
- }>, v.ObjectSchema<{
190
+ }>, v.OptionalSchema<v.ObjectSchema<{
191
191
  readonly lane: v.OptionalSchema<v.ObjectSchema<{
192
192
  readonly pane: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
193
193
  readonly session: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
@@ -197,7 +197,7 @@ export declare const taskSyncRouter: {
197
197
  }, undefined>, undefined>;
198
198
  readonly title: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
199
199
  readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
200
- }, undefined>, import("@orpc/contract").Schema<{
200
+ }, undefined>, undefined>, import("@orpc/contract").Schema<{
201
201
  task_id: string;
202
202
  created: boolean;
203
203
  }, {
@@ -426,6 +426,11 @@ 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 {
429
434
  // The SessionStart hook sends the pane + the CLI's native session id.
430
435
  // SESSION is the match key (kind_data.lane.session) — it is unique per
431
436
  // CLI session and re-minted each session, so a fresh session cannot find
@@ -599,6 +604,14 @@ export const taskSyncRouter = {
599
604
  });
600
605
  }
601
606
  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
+ }
602
615
  }),
603
616
 
604
617
  /**