Skip to content

Commit 939f6cb

Browse files
unexgedcramerclaude
authored
fix(types): Accept datetime with microsecond precision and timezone offsets (#115)
* fix(types): Accept datetime with microsecond precision and timezone offsets Python and other tools that agents may use emit timestamps with higher precision than JavaScript's native Date.toISOString(). Update the datetime schema to accept variable fractional seconds and timezone offsets. Co-authored-by: David Cramer <dcramer@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4e15a8a commit 939f6cb

File tree

2 files changed

+36
-12
lines changed

2 files changed

+36
-12
lines changed

src/core/storage/jsonl-storage.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ describe("JsonlStorage", () => {
135135
expect(() => storage.read()).toThrow(DataCorruptionError);
136136
});
137137

138+
it("accepts datetime with microsecond precision", () => {
139+
// Python and other tools that agents may use emit microsecond precision
140+
// which is valid ISO 8601 but more precise than JavaScript's native Date.toISOString()
141+
const task = createTask({
142+
id: "micro123",
143+
completed: true,
144+
completed_at: "2026-02-05T08:56:48.487608+00:00", // 6 decimal places
145+
created_at: "2026-02-05T08:56:48.487608+00:00",
146+
updated_at: "2026-02-05T08:56:48.487608+00:00",
147+
});
148+
const tasksFile = path.join(tempDir, "tasks.jsonl");
149+
fs.writeFileSync(tasksFile, JSON.stringify(task) + "\n", "utf-8");
150+
151+
const store = storage.read();
152+
expect(store.tasks).toHaveLength(1);
153+
expect(store.tasks[0].completed_at).toBe("2026-02-05T08:56:48.487608+00:00");
154+
});
155+
138156
it("throws StorageError when file cannot be read", () => {
139157
const tasksFile = path.join(tempDir, "tasks.jsonl");
140158
fs.writeFileSync(tasksFile, "test", "utf-8");

src/types.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import { z } from "zod";
33
// Maximum content length (50KB) to prevent excessive file sizes
44
const MAX_CONTENT_LENGTH = 50 * 1024;
55

6+
// ISO 8601 datetime that accepts timezone offsets (+00:00) in addition to Z suffix,
7+
// for compatibility with Python and other tools that agents may use to generate timestamps
8+
function flexibleDatetime() {
9+
return z.string().datetime({ offset: true });
10+
}
11+
612
export const CommitMetadataSchema = z.object({
713
sha: z.string().min(1),
814
message: z.string().optional(),
915
branch: z.string().optional(),
1016
url: z.string().url().optional(),
11-
timestamp: z.string().datetime().optional(),
17+
timestamp: flexibleDatetime().optional(),
1218
});
1319

1420
export type CommitMetadata = z.infer<typeof CommitMetadataSchema>;
@@ -66,10 +72,10 @@ const TaskSchemaBase = z.object({
6672
.nullable()
6773
.default(null),
6874
metadata: TaskMetadataSchema.default(null),
69-
created_at: z.string().datetime(),
70-
updated_at: z.string().datetime(),
71-
started_at: z.string().datetime().nullable().default(null),
72-
completed_at: z.string().datetime().nullable().default(null),
75+
created_at: flexibleDatetime(),
76+
updated_at: flexibleDatetime(),
77+
started_at: flexibleDatetime().nullable().default(null),
78+
completed_at: flexibleDatetime().nullable().default(null),
7379
// Bidirectional blocking relationships
7480
blockedBy: z.array(z.string().min(1)).default([]), // Tasks that block this one
7581
blocks: z.array(z.string().min(1)).default([]), // Tasks this one blocks
@@ -146,10 +152,10 @@ export const CreateTaskInputSchema = z.object({
146152
.nullable()
147153
.optional(),
148154
metadata: TaskMetadataSchema.optional(),
149-
created_at: z.string().datetime().optional(),
150-
updated_at: z.string().datetime().optional(),
151-
started_at: z.string().datetime().nullable().optional(),
152-
completed_at: z.string().datetime().nullable().optional(),
155+
created_at: flexibleDatetime().optional(),
156+
updated_at: flexibleDatetime().optional(),
157+
started_at: flexibleDatetime().nullable().optional(),
158+
completed_at: flexibleDatetime().nullable().optional(),
153159
});
154160

155161
export type CreateTaskInput = z.infer<typeof CreateTaskInputSchema>;
@@ -179,7 +185,7 @@ export const UpdateTaskInputSchema = z.object({
179185
.nullable()
180186
.optional(),
181187
metadata: TaskMetadataSchema.nullable().optional(),
182-
started_at: z.string().datetime().nullable().optional(),
188+
started_at: flexibleDatetime().nullable().optional(),
183189
delete: z.boolean().optional(),
184190
add_blocked_by: z.array(z.string().min(1)).optional(),
185191
remove_blocked_by: z.array(z.string().min(1)).optional(),
@@ -218,8 +224,8 @@ export const ArchivedTaskSchema = z.object({
218224
name: z.string().min(1, "Name is required"),
219225
description: z.string().default(""),
220226
result: z.string().nullable().default(null),
221-
completed_at: z.string().datetime().nullable().default(null),
222-
archived_at: z.string().datetime(),
227+
completed_at: flexibleDatetime().nullable().default(null),
228+
archived_at: flexibleDatetime(),
223229
metadata: z
224230
.object({
225231
github: GithubMetadataSchema.optional(),

0 commit comments

Comments
 (0)