@shipfox/api-logs-dto 12.0.0 → 16.0.0

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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-logs-dto",
3
3
  "license": "MIT",
4
- "version": "12.0.0",
4
+ "version": "16.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -15,10 +15,15 @@
15
15
  ".": {
16
16
  "types": "./dist/index.d.ts",
17
17
  "default": "./dist/index.js"
18
+ },
19
+ "./inter-module": {
20
+ "types": "./dist/inter-module.d.ts",
21
+ "default": "./dist/inter-module.js"
18
22
  }
19
23
  },
20
24
  "dependencies": {
21
- "zod": "^4.4.3"
25
+ "zod": "^4.4.3",
26
+ "@shipfox/inter-module": "0.2.3"
22
27
  },
23
28
  "imports": {
24
29
  "#*": "./dist/*"
package/src/index.ts CHANGED
@@ -11,7 +11,9 @@ export {
11
11
  appendLogsQuerySchema,
12
12
  appendLogsResponseSchema,
13
13
  type LogRecord,
14
+ type LogWriterConflictResponseDto,
14
15
  logRecordSchema,
16
+ logWriterConflictResponseSchema,
15
17
  MAX_RECORD_DATA_BYTES,
16
18
  MAX_RECORD_NAME_BYTES,
17
19
  type OffsetGapResponseDto,
@@ -25,6 +27,7 @@ export {
25
27
  readLogsQuerySchema,
26
28
  readLogsResponseSchema,
27
29
  SESSION_VIEW_VERSION,
30
+ type ServerLogRecord,
28
31
  type SessionViewDto,
29
32
  type SessionViewLifecycleRow,
30
33
  type SessionViewMessageRow,
@@ -34,6 +37,7 @@ export {
34
37
  type SessionViewThinkingRow,
35
38
  type SessionViewToolCallRow,
36
39
  type SessionViewToolResultRow,
40
+ serverLogRecordSchema,
37
41
  sessionViewLifecycleRowSchema,
38
42
  sessionViewMessageRowSchema,
39
43
  sessionViewRawRowSchema,
@@ -0,0 +1,69 @@
1
+ import {logsInterModuleContract} from './inter-module.js';
2
+
3
+ describe('logsInterModuleContract', () => {
4
+ test('accepts server-origin append commands', () => {
5
+ const input = logsInterModuleContract.methods.appendServerRecords.input.parse({
6
+ jobId: '00000000-0000-4000-8000-000000000001',
7
+ workspaceId: '00000000-0000-4000-8000-000000000002',
8
+ projectId: '00000000-0000-4000-8000-000000000003',
9
+ workflowRunAttemptId: '00000000-0000-4000-8000-000000000004',
10
+ stepId: '00000000-0000-4000-8000-000000000005',
11
+ attempt: 1,
12
+ records: [
13
+ {v: 1, ts: 1, type: 'output', stream: 'stdout', data: 'hello'},
14
+ {v: 1, ts: 1, type: 'group_start', group_id: 'g1', parent_group_id: null, name: 'call'},
15
+ {v: 1, ts: 1, type: 'group_end', group_id: 'g1'},
16
+ ],
17
+ });
18
+
19
+ expect(input.attempt).toBe(1);
20
+ expect(input.records).toHaveLength(3);
21
+ expect(input.records[1]).toMatchObject({type: 'group_start'});
22
+ });
23
+
24
+ test('rejects records outside the server-writable stored union', () => {
25
+ const result = logsInterModuleContract.methods.appendServerRecords.input.safeParse({
26
+ jobId: '00000000-0000-4000-8000-000000000001',
27
+ workspaceId: '00000000-0000-4000-8000-000000000002',
28
+ projectId: '00000000-0000-4000-8000-000000000003',
29
+ workflowRunAttemptId: '00000000-0000-4000-8000-000000000004',
30
+ stepId: '00000000-0000-4000-8000-000000000005',
31
+ attempt: 1,
32
+ records: [{v: 1, ts: 1, type: 'agent_session', data: 'raw entry, not yet normalized'}],
33
+ });
34
+
35
+ expect(result.success).toBe(false);
36
+ });
37
+
38
+ test('rejects server-only tombstones', () => {
39
+ const result = logsInterModuleContract.methods.appendServerRecords.input.safeParse({
40
+ jobId: '00000000-0000-4000-8000-000000000001',
41
+ workspaceId: '00000000-0000-4000-8000-000000000002',
42
+ projectId: '00000000-0000-4000-8000-000000000003',
43
+ workflowRunAttemptId: '00000000-0000-4000-8000-000000000004',
44
+ stepId: '00000000-0000-4000-8000-000000000005',
45
+ attempt: 1,
46
+ records: [{v: 1, ts: 1, type: 'capped'}],
47
+ });
48
+
49
+ expect(result.success).toBe(false);
50
+ });
51
+
52
+ test('keeps identity, attempt, and output bounds enforced', () => {
53
+ const input = {
54
+ jobId: '00000000-0000-4000-8000-000000000001',
55
+ workspaceId: '00000000-0000-4000-8000-000000000002',
56
+ projectId: '00000000-0000-4000-8000-000000000003',
57
+ workflowRunAttemptId: '00000000-0000-4000-8000-000000000004',
58
+ stepId: '00000000-0000-4000-8000-000000000005',
59
+ attempt: 1,
60
+ records: [],
61
+ };
62
+ const schema = logsInterModuleContract.methods.appendServerRecords;
63
+
64
+ expect(schema.input.safeParse({...input, attempt: 0}).success).toBe(false);
65
+ expect(schema.input.safeParse({...input, attempt: 2_147_483_648}).success).toBe(false);
66
+ expect(schema.input.safeParse({...input, jobId: 'not-a-uuid'}).success).toBe(false);
67
+ expect(schema.output.safeParse({committedLength: 0, capped: true}).success).toBe(true);
68
+ });
69
+ });
@@ -0,0 +1,69 @@
1
+ import {defineInterModuleContract, type InterModuleClient} from '@shipfox/inter-module';
2
+ import {z} from 'zod';
3
+ import {serverLogRecordSchema} from './schemas/index.js';
4
+
5
+ const idSchema = z.string().uuid();
6
+
7
+ /**
8
+ * Producer-owned Logs commands used by synchronous callers. The only method
9
+ * today is the server-origin append for server-executed steps (the tool step
10
+ * executor): it writes already-normalized stored records through the same
11
+ * offset-CAS and budget pipeline as the lease-bound runner route, but with
12
+ * chunk `origin` `server` and a tail-derived CAS offset (the caller owns no
13
+ * spool cursor). This is a trusted internal boundary, not an authorization
14
+ * boundary: the caller must derive the identity fields from its execution
15
+ * context rather than pass through arbitrary external input.
16
+ */
17
+ export const logsInterModuleContract = defineInterModuleContract({
18
+ module: 'logs',
19
+ methods: {
20
+ appendServerRecords: {
21
+ input: z.object({
22
+ jobId: idSchema,
23
+ workspaceId: idSchema,
24
+ projectId: idSchema,
25
+ workflowRunAttemptId: idSchema,
26
+ stepId: idSchema,
27
+ attempt: z
28
+ .number()
29
+ .int()
30
+ .min(1)
31
+ .max(2_147_483_647)
32
+ .describe('Attempt number of the step this batch belongs to.'),
33
+ /**
34
+ * Already-normalized server-writable stored records (the read union
35
+ * without server-only tombstones), serialized to whole newline-terminated
36
+ * NDJSON lines on ingest. Server-origin records skip the raw-to-stored
37
+ * normalization the runner path applies: they are stored verbatim. Callers should
38
+ * coalesce records into batches up to (but never over) `LOG_APPEND_BODY_LIMIT_BYTES`;
39
+ * an empty batch is reserved for a heartbeat. A server-origin writer owns its stream;
40
+ * it cannot be mixed with a lease-bound runner writer because their byte cursors differ.
41
+ */
42
+ records: z.array(serverLogRecordSchema),
43
+ }),
44
+ output: z.object({
45
+ committedLength: z
46
+ .number()
47
+ .int()
48
+ .min(0)
49
+ .describe(
50
+ 'New server-held byte position of the stream after this batch was applied (the tail the next call continues from).',
51
+ ),
52
+ capped: z
53
+ .boolean()
54
+ .describe(
55
+ 'When true, the per-job log budget is exhausted and further output is dropped.',
56
+ ),
57
+ }),
58
+ errors: {
59
+ 'lease-stream-mismatch': z.object({}),
60
+ 'malformed-log-chunk': z.object({}),
61
+ 'append-body-too-large': z.object({maxBytes: z.number().int().positive()}),
62
+ 'runner-writer-active': z.object({}),
63
+ 'offset-gap': z.object({committedLength: z.number().int().nonnegative()}),
64
+ },
65
+ },
66
+ },
67
+ });
68
+
69
+ export type LogsModuleClient = InterModuleClient<typeof logsInterModuleContract>;
@@ -53,3 +53,10 @@ export const offsetGapResponseSchema = z.object({
53
53
  });
54
54
 
55
55
  export type OffsetGapResponseDto = z.infer<typeof offsetGapResponseSchema>;
56
+
57
+ /** A lease append was attempted on a stream owned by a server-origin writer. */
58
+ export const logWriterConflictResponseSchema = z.object({
59
+ code: z.literal('log-writer-conflict'),
60
+ });
61
+
62
+ export type LogWriterConflictResponseDto = z.infer<typeof logWriterConflictResponseSchema>;
@@ -3,6 +3,8 @@ export {
3
3
  type AppendLogsResponseDto,
4
4
  appendLogsQuerySchema,
5
5
  appendLogsResponseSchema,
6
+ type LogWriterConflictResponseDto,
7
+ logWriterConflictResponseSchema,
6
8
  type OffsetGapResponseDto,
7
9
  offsetGapResponseSchema,
8
10
  } from './append.js';
@@ -21,6 +23,8 @@ export {
21
23
  parseRawLogRecordLine,
22
24
  type RawLogRecord,
23
25
  rawLogRecordSchema,
26
+ type ServerLogRecord,
27
+ serverLogRecordSchema,
24
28
  } from './record.js';
25
29
  export {
26
30
  SESSION_VIEW_VERSION,
@@ -70,7 +70,9 @@ const readLogsPresignedSchema = z.object({
70
70
  .number()
71
71
  .int()
72
72
  .min(0)
73
- .describe('Committed append byte position for the attempt. This is the runner CAS axis.'),
73
+ .describe(
74
+ 'Committed append byte position for the attempt. This is the active writer CAS axis.',
75
+ ),
74
76
  truncated: z
75
77
  .boolean()
76
78
  .describe('True when the stream was force-closed because the runner stopped reporting.'),
@@ -123,19 +123,27 @@ export const rawLogRecordSchema = z.discriminatedUnion('type', [
123
123
  ]);
124
124
 
125
125
  /** Stored/read records: regular records, normalized agent sessions, and tombstones. */
126
- export const logRecordSchema = z.discriminatedUnion('type', [
126
+ const storedLogRecordSchemas = [
127
127
  logOutput,
128
128
  logGroupStart,
129
129
  logGroupEnd,
130
130
  logEnd,
131
131
  logGap,
132
132
  agentSession,
133
+ ] as const;
134
+
135
+ export const logRecordSchema = z.discriminatedUnion('type', [
136
+ ...storedLogRecordSchemas,
133
137
  logCapped,
134
138
  logRunnerLost,
135
139
  ]);
136
140
 
141
+ /** Records a trusted server-origin writer may append: stored records without tombstones. */
142
+ export const serverLogRecordSchema = z.discriminatedUnion('type', storedLogRecordSchemas);
143
+
137
144
  export type RawLogRecord = z.infer<typeof rawLogRecordSchema>;
138
145
  export type LogRecord = z.infer<typeof logRecordSchema>;
146
+ export type ServerLogRecord = z.infer<typeof serverLogRecordSchema>;
139
147
 
140
148
  export function parseLogRecordLine(line: string): LogRecord {
141
149
  return logRecordSchema.parse(JSON.parse(line));