Skip to content

Commit a8a0e0c

Browse files
Move hook input deserialization next to the cast that types it
normalizeHookInput lived in client.ts and inspected for a 'timestamp' property by name, which felt magical (brittle against any future hook-shaped wire payload that happens to contain a numeric 'timestamp'). Move the conversion into CopilotSession._handleHooksInvoke, renamed deserializeHookInput, right next to the GenericHandler cast that says 'this unknown is now a HookInput'. That's the only call site that actually knows the payload is a hook input, so it's the correct boundary for the schema transform. This is the TS equivalent of what C# does via UnixMillisecondsDateTimeOffsetConverter (attached per-property on each HookInput.Timestamp); TS just plumbs the same conversion through the hooks dispatcher instead of a per-type JSON converter. Verified 3/3 hooks_extended.e2e tests pass locally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 11df3b1 commit a8a0e0c

2 files changed

Lines changed: 27 additions & 23 deletions

File tree

nodejs/src/client.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -87,23 +87,6 @@ function isZodSchema(value: unknown): value is { toJSONSchema(): Record<string,
8787
);
8888
}
8989

90-
/**
91-
* Normalize an inbound hook input payload by converting the numeric
92-
* Unix-ms `timestamp` field (as sent on the wire) into a `Date`.
93-
*/
94-
function normalizeHookInput(input: unknown): unknown {
95-
if (
96-
input &&
97-
typeof input === "object" &&
98-
"timestamp" in input &&
99-
typeof (input as { timestamp: unknown }).timestamp === "number"
100-
) {
101-
const t = (input as { timestamp: number }).timestamp;
102-
return { ...(input as Record<string, unknown>), timestamp: new Date(t) };
103-
}
104-
return input;
105-
}
106-
10790
/**
10891
* Convert tool parameters to JSON schema format for sending to CLI
10992
*/
@@ -2059,10 +2042,7 @@ export class CopilotClient {
20592042
throw new Error(`Session not found: ${params.sessionId}`);
20602043
}
20612044

2062-
const output = await session._handleHooksInvoke(
2063-
params.hookType,
2064-
normalizeHookInput(params.input)
2065-
);
2045+
const output = await session._handleHooksInvoke(params.hookType, params.input);
20662046
return { output };
20672047
}
20682048

nodejs/src/session.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@ import type {
5454
export const NO_RESULT_PERMISSION_V2_ERROR =
5555
"Permission handlers cannot return 'no-result' when connected to a protocol v2 server.";
5656

57+
/**
58+
* Convert a raw hook input received over the wire into its public-facing shape.
59+
* Currently this only deserializes the numeric Unix-ms `timestamp` field on
60+
* BaseHookInput into a Date. Anything else passes through unchanged.
61+
*/
62+
function deserializeHookInput(raw: unknown): unknown {
63+
if (
64+
!raw ||
65+
typeof raw !== "object" ||
66+
typeof (raw as { timestamp?: unknown }).timestamp !== "number"
67+
) {
68+
return raw;
69+
}
70+
const obj = raw as Record<string, unknown> & { timestamp: number };
71+
return { ...obj, timestamp: new Date(obj.timestamp) };
72+
}
73+
5774
/** Assistant message event - the final response from the assistant. */
5875
export type AssistantMessageEvent = Extract<SessionEvent, { type: "assistant.message" }>;
5976

@@ -955,7 +972,14 @@ export class CopilotSession {
955972
return undefined;
956973
}
957974

958-
// Type-safe handler lookup with explicit casting
975+
// All hook inputs share BaseHookInput, which exposes `timestamp` as a Date.
976+
// The wire format sends it as Unix epoch ms (number), so we deserialize
977+
// here, at the one place that knows the input is a hook payload. Bad data
978+
// is left alone — the user-facing handler types still cast unknown to the
979+
// specific HookInput, so a runtime type mismatch surfaces as a normal
980+
// TypeError in user code rather than being silently masked.
981+
const normalized = deserializeHookInput(input);
982+
959983
type GenericHandler = (
960984
input: unknown,
961985
invocation: { sessionId: string }
@@ -976,7 +1000,7 @@ export class CopilotSession {
9761000
}
9771001

9781002
try {
979-
const result = await handler(input, { sessionId: this.sessionId });
1003+
const result = await handler(normalized, { sessionId: this.sessionId });
9801004
return result;
9811005
} catch (_error) {
9821006
// Hook failed, return undefined

0 commit comments

Comments
 (0)