@boxxy/model 0.1.8 → 0.2.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.
@@ -87,7 +87,9 @@ export interface WorkFacts {
87
87
  readonly skillsRef: CommitSha | null;
88
88
  readonly rulesVersion: RulesVersion;
89
89
  };
90
- 'boxxy.decision.recorded': DecisionRow;
90
+ 'boxxy.decision.recorded': Omit<DecisionRow, 'storeVersion'> & {
91
+ readonly snapshotStoreVersion: number;
92
+ };
91
93
  'boxxy.attempt.started': OfAttempt & {
92
94
  readonly atcSession: AtcSessionId;
93
95
  readonly resumeFallback: boolean;
@@ -99,7 +99,6 @@ export declare const workFactSchemas: {
99
99
  pool: z.core.$ZodBranded<z.ZodString, "PoolName", "out">;
100
100
  rulesVersion: z.core.$ZodBranded<z.core.$ZodBranded<z.ZodString, "ContentHash", "out">, "RulesVersion", "out">;
101
101
  snapshotHash: z.core.$ZodBranded<z.core.$ZodBranded<z.ZodString, "ContentHash", "out">, "SnapshotHash", "out">;
102
- storeVersion: z.ZodNumber;
103
102
  pepysOffsets: z.ZodRecord<z.core.$ZodBranded<z.ZodString, "StreamName", "out">, z.ZodNumber>;
104
103
  faults: z.ZodArray<z.ZodObject<{
105
104
  plugin: z.ZodString;
@@ -120,6 +119,7 @@ export declare const workFactSchemas: {
120
119
  max: z.ZodNumber;
121
120
  }, z.core.$strip>>;
122
121
  at: z.core.$ZodBranded<z.ZodNumber, "Instant", "out">;
122
+ snapshotStoreVersion: z.ZodNumber;
123
123
  }, z.core.$strip>;
124
124
  'boxxy.attempt.started': z.ZodObject<{
125
125
  attempt: z.core.$ZodBranded<z.ZodString, "AttemptId", "out">;
package/dist/index.d.ts CHANGED
@@ -58,7 +58,7 @@ export type { AcceptanceInput, BuildCloseOut, BuildInput, CloseOut, CloseOutAnsw
58
58
  export type { Claim } from './dispatch/types.ts';
59
59
  export type { AnyNewFact, EntityRef, FactEnvelope, FactType, NewFact, OutboxFact, RelationPayload, WorkFacts, } from './facts/types.ts';
60
60
  export type { Admission, AdmissionGrant, AdmissionName, Decision, Fault, Filter, Frame, FrameItem, FrameValues, Gate, GateName, Hold, Lift, Limit, LimitFill, Pick, PlanContext, Pool, PropagationLine, SeatRef, SeatRelease, SeatTake, Signal, SignalKey, SignalName, Snapshot, StartPick, View, ViewName, Wait, WaitReason, Waiver, WaiverName, Wake, WakePick, } from './planning/types.ts';
61
- export type { ApplyResult, Attention, CallerProof, CapacityBackoff, ClaimStore, Clock, FactSink, ForgetOutcome, Forge, GitSource, HoldNotice, ItemDetailRows, ItemFilter, NewId, NoticeOutcome, Notifier, OutboxReader, PepysAnswer, PepysReader, SinkAnswer, PullRequestReading, RowWrite, SessionFault, SessionReading, SpawnRequest, StartOutcome, StopOutcome, StoreChange, StoreReader, StoreTx, SuspendOutcome, TableName, Tables, WakeSource, WorkerKind, } from './ports/types.ts';
61
+ export type { ApplyResult, Attention, CallerProof, CapacityBackoff, ClaimStore, Clock, FactSink, ForgetOutcome, Forge, GitSource, HoldNotice, ItemDetailRows, ItemFilter, NewId, NoticeOutcome, NoticeTryUpdate, Notifier, OutboxReader, PepysAnswer, PepysReader, SinkAnswer, PullRequestReading, RowWrite, RunnerFault, SessionFault, SessionReading, SpawnRequest, StartOutcome, StopOutcome, StoreChange, StoreReader, StoreTx, SuspendOutcome, TableName, Tables, WakeSource, WorkerKind, WorkerRefusal, } from './ports/types.ts';
62
62
  export type { CostToLand, StageTime } from './reports/types.ts';
63
63
  export type { OutboxStatus } from './reports/outbox-status.ts';
64
64
  export type { ItemSetRows, ItemWithLevers } from './items/types.ts';
@@ -32,6 +32,7 @@ export interface StoreReader extends OutboxReader {
32
32
  listLimitSeats: () => Promise<LimitSeatRow[]>;
33
33
  listItemNotes: (item: ItemId) => Promise<NoteRow[]>;
34
34
  listUnsentNotices: (limit: number) => Promise<NoticeRow[]>;
35
+ listNoticesDue: (now: Instant, limit: number) => Promise<NoticeRow[]>;
35
36
  listKeptSessions: () => Promise<{
36
37
  readonly item: ItemId;
37
38
  readonly kept: KeptSession;
@@ -67,7 +68,7 @@ export interface StoreTx extends StoreReader {
67
68
  writeHeartbeat: (runner: RunnerId, at: Instant) => Promise<void>;
68
69
  syncLimits: (limits: readonly Limit[], at: Instant) => Promise<void>;
69
70
  markPullRequestChecked: (ref: PullRequestRef, at: Instant) => Promise<void>;
70
- markNoticeTried: (id: number, at: Instant, sent: boolean) => Promise<void>;
71
+ markNoticeTried: (id: number, update: NoticeTryUpdate) => Promise<void>;
71
72
  }
72
73
  export interface StoreChange {
73
74
  readonly at: Instant;
@@ -204,6 +205,9 @@ export type Attention = 'working' | 'needs-input' | 'turn-ended';
204
205
  export interface GitSource {
205
206
  readonly url: string;
206
207
  readonly ref: string;
208
+ readonly credentialRef?: {
209
+ readonly name: string;
210
+ };
207
211
  }
208
212
  export type SpawnRequest = SpawnRequestFields & ({
209
213
  readonly cwd: string;
@@ -227,13 +231,23 @@ export interface HoldNotice {
227
231
  readonly item: ItemId;
228
232
  readonly reason: HoldReason;
229
233
  readonly at: Instant;
234
+ readonly key: string;
230
235
  }
231
236
  export type NoticeOutcome = {
232
237
  readonly kind: 'sent';
233
238
  } | {
234
- readonly kind: 'failed';
239
+ readonly kind: 'refused';
240
+ readonly reason: string;
241
+ } | {
242
+ readonly kind: 'uncertain';
235
243
  readonly reason: string;
236
244
  };
245
+ export interface NoticeTryUpdate {
246
+ readonly sentAt: Instant | null;
247
+ readonly keySeq: number;
248
+ readonly keyTries: number;
249
+ readonly nextTryAt: Instant;
250
+ }
237
251
  export interface CallerProof {
238
252
  prove: (request: Request) => Promise<CallerIdentity | null>;
239
253
  }
@@ -248,6 +262,15 @@ export interface SessionFault {
248
262
  readonly attempt: AttemptId;
249
263
  readonly holder: AttemptId;
250
264
  }
265
+ export interface WorkerRefusal {
266
+ readonly kind: 'fault';
267
+ readonly fault: 'spawn-refused' | 'wake-refused';
268
+ readonly attempt: AttemptId;
269
+ readonly item: ItemId;
270
+ readonly cause: 'capacity' | 'other';
271
+ readonly reason: string;
272
+ }
273
+ export type RunnerFault = SessionFault | WorkerRefusal;
251
274
  export type SinkAnswer = {
252
275
  readonly kind: 'stored';
253
276
  } | {
@@ -1,9 +1,14 @@
1
- import type { HoldNotice, Notifier } from '../ports/types.ts';
1
+ import type { HoldNotice, NoticeOutcome, Notifier } from '../ports/types.ts';
2
2
  export interface FakeNotifier extends Notifier {
3
3
  readonly told: readonly HoldNotice[];
4
+ readonly attempts: readonly HoldNotice[];
4
5
  readonly calls: () => number;
5
- failNext: (reason: string) => void;
6
- failAlways: (reason: string) => void;
6
+ failNext: (outcome: Exclude<NoticeOutcome, {
7
+ kind: 'sent';
8
+ }>) => void;
9
+ failAlways: (outcome: Exclude<NoticeOutcome, {
10
+ kind: 'sent';
11
+ }>) => void;
7
12
  recover: () => void;
8
13
  }
9
14
  export declare function createFakeNotifier(): FakeNotifier;
package/dist/types.d.ts CHANGED
@@ -261,6 +261,9 @@ export interface NoticeRow {
261
261
  readonly at: Instant;
262
262
  readonly tries: number;
263
263
  readonly sentAt: Instant | null;
264
+ readonly keySeq: number;
265
+ readonly keyTries: number;
266
+ readonly nextTryAt: Instant;
264
267
  }
265
268
  export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
266
269
  export interface Recipe {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxxy/model",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "The types that boxxy's packages share",
5
5
  "homepage": "https://github.com/zgeoff/boxxy/tree/main/libs/model#readme",
6
6
  "bugs": "https://github.com/zgeoff/boxxy/issues",
@@ -31,7 +31,7 @@
31
31
  "typecheck": "tsrs --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@boxxy/content-hash": "0.1.8",
34
+ "@boxxy/content-hash": "0.2.0",
35
35
  "tiny-invariant": "1.3.3",
36
36
  "zod": "4.6.7"
37
37
  },
@@ -133,7 +133,11 @@ export interface WorkFacts {
133
133
  readonly skillsRef: CommitSha | null;
134
134
  readonly rulesVersion: RulesVersion;
135
135
  };
136
- 'boxxy.decision.recorded': DecisionRow;
136
+
137
+ // The sink adds `storeVersion` to every payload, so the decision's own field takes another name.
138
+ 'boxxy.decision.recorded': Omit<DecisionRow, 'storeVersion'> & {
139
+ readonly snapshotStoreVersion: number;
140
+ };
137
141
  'boxxy.attempt.started': OfAttempt & {
138
142
  readonly atcSession: AtcSessionId;
139
143
  readonly resumeFallback: boolean;
@@ -93,7 +93,9 @@ export const workFactSchemas = {
93
93
  skillsRef: brandSchemas.commitSha.nullable(),
94
94
  rulesVersion: brandSchemas.rulesVersion,
95
95
  }),
96
- 'boxxy.decision.recorded': decisionRowSchema,
96
+ 'boxxy.decision.recorded': decisionRowSchema
97
+ .omit({ storeVersion: true })
98
+ .extend({ snapshotStoreVersion: countSchema }),
97
99
  'boxxy.attempt.started': z.object({
98
100
  ...ofAttempt,
99
101
  atcSession: brandSchemas.atcSessionId,
package/src/index.ts CHANGED
@@ -229,6 +229,7 @@ export type {
229
229
  ItemFilter,
230
230
  NewId,
231
231
  NoticeOutcome,
232
+ NoticeTryUpdate,
232
233
  Notifier,
233
234
  OutboxReader,
234
235
  PepysAnswer,
@@ -236,6 +237,7 @@ export type {
236
237
  SinkAnswer,
237
238
  PullRequestReading,
238
239
  RowWrite,
240
+ RunnerFault,
239
241
  SessionFault,
240
242
  SessionReading,
241
243
  SpawnRequest,
@@ -249,6 +251,7 @@ export type {
249
251
  Tables,
250
252
  WakeSource,
251
253
  WorkerKind,
254
+ WorkerRefusal,
252
255
  } from './ports/types.ts';
253
256
 
254
257
  export type { CostToLand, StageTime } from './reports/types.ts';
@@ -95,6 +95,9 @@ export interface StoreReader extends OutboxReader {
95
95
  // the rows not yet sent, whatever their tries, oldest first
96
96
  listUnsentNotices: (limit: number) => Promise<NoticeRow[]>;
97
97
 
98
+ // the rows not yet sent whose `nextTryAt` has passed, oldest `nextTryAt` first
99
+ listNoticesDue: (now: Instant, limit: number) => Promise<NoticeRow[]>;
100
+
98
101
  // oldest kept first
99
102
  listKeptSessions: () => Promise<{ readonly item: ItemId; readonly kept: KeptSession }[]>;
100
103
 
@@ -152,8 +155,8 @@ export interface StoreTx extends StoreReader {
152
155
  syncLimits: (limits: readonly Limit[], at: Instant) => Promise<void>;
153
156
  markPullRequestChecked: (ref: PullRequestRef, at: Instant) => Promise<void>;
154
157
 
155
- // one more try of the notice; `sent` also sets its `sentAt`
156
- markNoticeTried: (id: number, at: Instant, sent: boolean) => Promise<void>;
158
+ // one more try of the notice, with the new key state and next try time; a `sentAt` also marks it sent
159
+ markNoticeTried: (id: number, update: NoticeTryUpdate) => Promise<void>;
157
160
  }
158
161
 
159
162
  // A change may write no fact, such as the suspend mark; such a change moves no store version. The
@@ -305,11 +308,13 @@ export type SessionReading =
305
308
 
306
309
  export type Attention = 'working' | 'needs-input' | 'turn-ended';
307
310
 
308
- // A git repository that atc clones on the target. It carries no credential: an imp clones over its
309
- // own network, where the broker adds the credential from the session's grants.
311
+ // A git repository that atc clones for the session. The atc daemon clones it before the session
312
+ // exists, so a private repository needs `credentialRef`: the name of the daemon's environment
313
+ // variable that holds a clone token. The name is no secret; boxxy never reads the token.
310
314
  export interface GitSource {
311
315
  readonly url: string;
312
316
  readonly ref: string;
317
+ readonly credentialRef?: { readonly name: string };
313
318
  }
314
319
 
315
320
  // Exactly one of the two: `cwd`, the repo's folder on a target on the atc daemon's own host, or
@@ -341,11 +346,27 @@ export interface HoldNotice {
341
346
  readonly item: ItemId;
342
347
  readonly reason: HoldReason;
343
348
  readonly at: Instant;
349
+
350
+ // the row's current key; the adapter sends it unchanged
351
+ readonly key: string;
344
352
  }
345
353
 
346
354
  export type NoticeOutcome =
347
355
  | { readonly kind: 'sent' }
348
- | { readonly kind: 'failed'; readonly reason: string };
356
+
357
+ // atc ran nothing
358
+ | { readonly kind: 'refused'; readonly reason: string }
359
+
360
+ // atc may have sent the message
361
+ | { readonly kind: 'uncertain'; readonly reason: string };
362
+
363
+ // What one try writes to its row, besides one more `tries`.
364
+ export interface NoticeTryUpdate {
365
+ readonly sentAt: Instant | null;
366
+ readonly keySeq: number;
367
+ readonly keyTries: number;
368
+ readonly nextTryAt: Instant;
369
+ }
349
370
 
350
371
  // Proves who sent a request to the server. The server's root middleware calls it for every
351
372
  // session and hook procedure, and knows no header and no token itself. null: no proof, so the
@@ -370,6 +391,21 @@ export interface SessionFault {
370
391
  readonly holder: AttemptId;
371
392
  }
372
393
 
394
+ // A spawn or a wake that the worker refused. A refused spawn settles the attempt as `no-capacity`
395
+ // or `spawn-refused`; a refused wake sleeps the attempt again for capacity, and otherwise leaves it
396
+ // `waking` for the next reading. The runner logs the worker's reason, which no fact carries.
397
+ export interface WorkerRefusal {
398
+ readonly kind: 'fault';
399
+ readonly fault: 'spawn-refused' | 'wake-refused';
400
+ readonly attempt: AttemptId;
401
+ readonly item: ItemId;
402
+ readonly cause: 'capacity' | 'other';
403
+ readonly reason: string;
404
+ }
405
+
406
+ // What a claim or a reading returns for the runner to log.
407
+ export type RunnerFault = SessionFault | WorkerRefusal;
408
+
373
409
  // What one batch sent to the sink got back. `retry` may or may not have stored the batch.
374
410
  export type SinkAnswer =
375
411
  | { readonly kind: 'stored' }
@@ -17,7 +17,7 @@ const limitCountSchema = z.object({
17
17
  max: countSchema,
18
18
  });
19
19
 
20
- // A plain object, so that the wire schema of `boxxy.decision.recorded` can extend it.
20
+ // A plain object, so that the schema of `boxxy.decision.recorded` can rename one field of it.
21
21
  export const decisionRowSchema = z.object({
22
22
  id: brandSchemas.decisionId,
23
23
  attempt: brandSchemas.attemptId,
@@ -14,4 +14,7 @@ export const noticeRowSchema: z.ZodType<NoticeRow> = z.object({
14
14
  at: brandSchemas.instant,
15
15
  tries: countSchema,
16
16
  sentAt: brandSchemas.instant.nullable(),
17
+ keySeq: countSchema,
18
+ keyTries: countSchema,
19
+ nextTryAt: brandSchemas.instant,
17
20
  });
@@ -14,6 +14,16 @@ export const gitSourceSchema: z.ZodType<GitSource> = z.strictObject({
14
14
  .min(1)
15
15
  .max(255)
16
16
  .refine((ref) => !ref.startsWith('-'), 'a git source ref must not start with -'),
17
+
18
+ // atc's own rule for the name of an environment variable
19
+ credentialRef: z
20
+ .strictObject({
21
+ name: z
22
+ .string()
23
+ .max(255)
24
+ .regex(/^[A-Za-z_]\w*$/u, 'a credentialRef names an environment variable'),
25
+ })
26
+ .exactOptional(),
17
27
  });
18
28
 
19
29
  const spawnRequestFields = z.strictObject({
@@ -4,46 +4,48 @@ export interface FakeNotifier extends Notifier {
4
4
  // each notice that was sent, in order
5
5
  readonly told: readonly HoldNotice[];
6
6
 
7
- // the number of calls, sent or failed
7
+ // every call, sent or not, in order
8
+ readonly attempts: readonly HoldNotice[];
9
+
10
+ // the number of calls, sent or not
8
11
  readonly calls: () => number;
9
12
 
10
- // the next call fails with the reason
11
- failNext: (reason: string) => void;
13
+ // the next call ends with the outcome
14
+ failNext: (outcome: Exclude<NoticeOutcome, { kind: 'sent' }>) => void;
12
15
 
13
- // every call fails with the reason until `recover`
14
- failAlways: (reason: string) => void;
16
+ // every call ends with the outcome until `recover`
17
+ failAlways: (outcome: Exclude<NoticeOutcome, { kind: 'sent' }>) => void;
15
18
  recover: () => void;
16
19
  }
17
20
 
18
21
  export function createFakeNotifier(): FakeNotifier {
19
22
  const told: HoldNotice[] = [];
20
- let calls = 0;
21
- let next: string | null = null;
22
- let always: string | null = null;
23
+ const attempts: HoldNotice[] = [];
24
+ let next: NoticeOutcome | null = null;
25
+ let always: NoticeOutcome | null = null;
23
26
 
24
27
  return {
25
28
  told,
26
- calls: () => calls,
27
- failNext: (reason) => {
28
- next = reason;
29
+ attempts,
30
+ calls: () => attempts.length,
31
+ failNext: (outcome) => {
32
+ next = outcome;
29
33
  },
30
- failAlways: (reason) => {
31
- always = reason;
34
+ failAlways: (outcome) => {
35
+ always = outcome;
32
36
  },
33
37
  recover: () => {
34
38
  always = null;
35
39
  },
36
40
  tellHold: (notice) => {
37
- calls += 1;
41
+ attempts.push(notice);
38
42
 
39
- const reason = next ?? always;
43
+ const outcome = next ?? always;
40
44
 
41
45
  next = null;
42
46
 
43
- if (reason !== null) {
44
- const failed: NoticeOutcome = { kind: 'failed', reason };
45
-
46
- return Promise.resolve(failed);
47
+ if (outcome !== null) {
48
+ return Promise.resolve(outcome);
47
49
  }
48
50
 
49
51
  told.push(notice);
package/src/types.ts CHANGED
@@ -386,7 +386,7 @@ export interface RunnerRow {
386
386
  readonly paused: boolean;
387
387
  }
388
388
 
389
- // A hold notice for the `Notifier` port. The tick tries it again until `tries` reaches 5.
389
+ // A hold notice for the `Notifier` port. The tick tries it again, with no cap, until atc takes it.
390
390
  export interface NoticeRow {
391
391
  readonly id: number;
392
392
  readonly item: ItemId;
@@ -394,6 +394,15 @@ export interface NoticeRow {
394
394
  readonly at: Instant;
395
395
  readonly tries: number;
396
396
  readonly sentAt: Instant | null;
397
+
398
+ // the row's current key is `boxxy-notice:<id>:<keySeq>`
399
+ readonly keySeq: number;
400
+
401
+ // uncertain tries under the current key; 0 after each change of key
402
+ readonly keyTries: number;
403
+
404
+ // a tick sends the row once now >= nextTryAt; a new row holds its `at`
405
+ readonly nextTryAt: Instant;
397
406
  }
398
407
 
399
408
  export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';