@boxxy/dispatch 0.1.0 → 0.1.3
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/README.md +4 -1
- package/dist/attempts/attempt-machine.d.ts +804 -0
- package/dist/attempts/attempt-moves.d.ts +2 -0
- package/dist/attempts/bind-attempt-session.d.ts +14 -0
- package/dist/attempts/build-attempt-change.d.ts +73 -0
- package/dist/attempts/build-attempt-fact.d.ts +3 -0
- package/dist/attempts/end-item-attempts.d.ts +18 -0
- package/dist/attempts/is-same-guard.d.ts +5 -0
- package/dist/attempts/pick-call-attempt.d.ts +10 -0
- package/dist/attempts/stop-stray-session.d.ts +11 -0
- package/dist/attempts/types.d.ts +142 -0
- package/dist/attempts/write-attempt-move.d.ts +7 -0
- package/dist/attempts/write-session-bind.d.ts +3 -0
- package/dist/claims/build-snapshot-hash.d.ts +2 -0
- package/dist/claims/claim-next.d.ts +6 -0
- package/dist/claims/pick-kept-session.d.ts +9 -0
- package/dist/claims/request-wake.d.ts +3 -0
- package/dist/claims/resume-claim.d.ts +3 -0
- package/dist/claims/start-claim.d.ts +3 -0
- package/dist/claims/types.d.ts +44 -0
- package/dist/closeouts/build-settle-plans.d.ts +17 -0
- package/dist/closeouts/close-out-attempt.d.ts +3 -0
- package/dist/closeouts/effect-handlers.d.ts +2 -0
- package/dist/closeouts/pick-settlement.d.ts +14 -0
- package/dist/closeouts/settle-attempt.d.ts +3 -0
- package/dist/closeouts/types.d.ts +50 -0
- package/dist/index.d.ts +38 -0
- package/dist/notices/constants.d.ts +4 -0
- package/dist/notices/create-null-notifier.d.ts +2 -0
- package/dist/notices/send-notices.d.ts +2 -0
- package/dist/notices/types.d.ts +11 -0
- package/dist/outbox/build-backoff-ms.d.ts +2 -0
- package/dist/outbox/build-initial-sender-state.d.ts +2 -0
- package/dist/outbox/build-outbox-status.d.ts +15 -0
- package/dist/outbox/constants.d.ts +2 -0
- package/dist/outbox/measure-wire-bytes.d.ts +2 -0
- package/dist/outbox/pick-sender-move.d.ts +10 -0
- package/dist/outbox/run-sender-once.d.ts +2 -0
- package/dist/outbox/take-batch.d.ts +3 -0
- package/dist/outbox/types.d.ts +61 -0
- package/dist/pull-requests/build-link-change.d.ts +11 -0
- package/dist/pull-requests/build-state-fact.d.ts +8 -0
- package/dist/pull-requests/collect-attempt-trailers.d.ts +2 -0
- package/dist/pull-requests/collect-seen-state.d.ts +7 -0
- package/dist/pull-requests/constants.d.ts +3 -0
- package/dist/pull-requests/link-pull-request.d.ts +2 -0
- package/dist/pull-requests/parse-pull-request-url.d.ts +2 -0
- package/dist/pull-requests/types.d.ts +37 -0
- package/dist/pull-requests/watch-pull-requests.d.ts +2 -0
- package/dist/readings/act-on-reading.d.ts +3 -0
- package/dist/readings/find-waiting-since.d.ts +9 -0
- package/dist/readings/pick-reading-action.d.ts +2 -0
- package/dist/readings/types.d.ts +79 -0
- package/dist/sessions/build-keep-writes.d.ts +9 -0
- package/dist/sessions/build-kept-clear.d.ts +2 -0
- package/dist/sessions/forget-sessions.d.ts +2 -0
- package/dist/sessions/is-kept-due.d.ts +2 -0
- package/dist/sessions/resume-kept-session.d.ts +6 -0
- package/dist/sessions/types.d.ts +19 -0
- package/dist/test-utils/apply-memory-change.d.ts +3 -0
- package/dist/test-utils/build-memory-reader.d.ts +3 -0
- package/dist/test-utils/build-mock-bind-change.d.ts +7 -0
- package/dist/test-utils/build-mock-claim-change.d.ts +2 -0
- package/dist/test-utils/build-mock-file-item-change.d.ts +2 -0
- package/dist/test-utils/build-mock-lever-change.d.ts +2 -0
- package/dist/test-utils/build-mock-new-id.d.ts +2 -0
- package/dist/test-utils/build-mock-note-change.d.ts +2 -0
- package/dist/test-utils/build-mock-pause-change.d.ts +2 -0
- package/dist/test-utils/build-mock-pull-request-change.d.ts +8 -0
- package/dist/test-utils/build-mock-relation-change.d.ts +7 -0
- package/dist/test-utils/build-mock-running-reading.d.ts +6 -0
- package/dist/test-utils/build-mock-settle-change.d.ts +7 -0
- package/dist/test-utils/build-mock-start-change.d.ts +2 -0
- package/dist/test-utils/claim-store-attempt-reader-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-claim-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-close-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-contract.d.ts +2 -0
- package/dist/test-utils/claim-store-copy-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-exclusive-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-guard-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-item-reader-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-outbox-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-race-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-rollback-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-seat-cases.d.ts +2 -0
- package/dist/test-utils/claim-store-session-cases.d.ts +2 -0
- package/dist/test-utils/collect-memory-limit-fills.d.ts +3 -0
- package/dist/test-utils/compare-text.d.ts +1 -0
- package/dist/test-utils/create-fake-backoff.d.ts +2 -0
- package/dist/test-utils/create-fake-clock.d.ts +5 -0
- package/dist/test-utils/create-fake-fact-sink.d.ts +8 -0
- package/dist/test-utils/create-fake-forge.d.ts +17 -0
- package/dist/test-utils/create-fake-worker.d.ts +42 -0
- package/dist/test-utils/create-memory-claim-store.d.ts +2 -0
- package/dist/test-utils/create-test-dispatch.d.ts +46 -0
- package/dist/test-utils/forge-contract.d.ts +15 -0
- package/dist/test-utils/is-same-held-seat.d.ts +2 -0
- package/dist/test-utils/types.d.ts +19 -0
- package/dist/test-utils/worker-contract.d.ts +14 -0
- package/dist/timeouts.d.ts +2 -0
- package/dist/types.d.ts +13 -0
- package/package.json +15 -2
- package/src/attempts/attempt-machine.ts +147 -0
- package/src/attempts/attempt-moves.ts +218 -0
- package/src/attempts/bind-attempt-session.ts +61 -0
- package/src/attempts/build-attempt-change.ts +540 -0
- package/src/attempts/build-attempt-fact.ts +135 -0
- package/src/attempts/end-item-attempts.ts +95 -0
- package/src/attempts/is-same-guard.ts +22 -0
- package/src/attempts/pick-call-attempt.ts +32 -0
- package/src/attempts/stop-stray-session.ts +48 -0
- package/src/attempts/types.ts +231 -0
- package/src/attempts/write-attempt-move.ts +41 -0
- package/src/attempts/write-session-bind.ts +47 -0
- package/src/claims/build-snapshot-hash.ts +9 -0
- package/src/claims/claim-next.ts +306 -0
- package/src/claims/pick-kept-session.ts +41 -0
- package/src/claims/request-wake.ts +17 -0
- package/src/claims/resume-claim.ts +56 -0
- package/src/claims/start-claim.ts +99 -0
- package/src/claims/types.ts +67 -0
- package/src/closeouts/build-settle-plans.ts +173 -0
- package/src/closeouts/close-out-attempt.ts +156 -0
- package/src/closeouts/effect-handlers.ts +232 -0
- package/src/closeouts/pick-settlement.ts +80 -0
- package/src/closeouts/settle-attempt.ts +161 -0
- package/src/closeouts/types.ts +87 -0
- package/src/index.ts +61 -0
- package/src/notices/constants.ts +7 -0
- package/src/notices/create-null-notifier.ts +6 -0
- package/src/notices/send-notices.ts +41 -0
- package/src/notices/types.ts +14 -0
- package/src/outbox/build-backoff-ms.ts +14 -0
- package/src/outbox/build-initial-sender-state.ts +14 -0
- package/src/outbox/build-outbox-status.ts +43 -0
- package/src/outbox/constants.ts +15 -0
- package/src/outbox/measure-wire-bytes.ts +31 -0
- package/src/outbox/pick-sender-move.ts +37 -0
- package/src/outbox/run-sender-once.ts +174 -0
- package/src/outbox/take-batch.ts +35 -0
- package/src/outbox/types.ts +76 -0
- package/src/pull-requests/build-link-change.ts +95 -0
- package/src/pull-requests/build-state-fact.ts +39 -0
- package/src/pull-requests/collect-attempt-trailers.ts +20 -0
- package/src/pull-requests/collect-seen-state.ts +19 -0
- package/src/pull-requests/constants.ts +5 -0
- package/src/pull-requests/link-pull-request.ts +119 -0
- package/src/pull-requests/parse-pull-request-url.ts +30 -0
- package/src/pull-requests/types.ts +63 -0
- package/src/pull-requests/watch-pull-requests.ts +308 -0
- package/src/readings/act-on-reading.ts +318 -0
- package/src/readings/find-waiting-since.ts +46 -0
- package/src/readings/pick-reading-action.ts +315 -0
- package/src/readings/types.ts +98 -0
- package/src/sessions/build-keep-writes.ts +55 -0
- package/src/sessions/build-kept-clear.ts +12 -0
- package/src/sessions/forget-sessions.ts +106 -0
- package/src/sessions/is-kept-due.ts +27 -0
- package/src/sessions/resume-kept-session.ts +25 -0
- package/src/sessions/types.ts +39 -0
- package/src/test-utils/apply-memory-change.ts +252 -0
- package/src/test-utils/build-memory-reader.ts +364 -0
- package/src/test-utils/build-mock-bind-change.ts +43 -0
- package/src/test-utils/build-mock-claim-change.ts +52 -0
- package/src/test-utils/build-mock-file-item-change.ts +20 -0
- package/src/test-utils/build-mock-lever-change.ts +18 -0
- package/src/test-utils/build-mock-new-id.ts +12 -0
- package/src/test-utils/build-mock-note-change.ts +18 -0
- package/src/test-utils/build-mock-pause-change.ts +19 -0
- package/src/test-utils/build-mock-pull-request-change.ts +63 -0
- package/src/test-utils/build-mock-relation-change.ts +40 -0
- package/src/test-utils/build-mock-running-reading.ts +19 -0
- package/src/test-utils/build-mock-settle-change.ts +70 -0
- package/src/test-utils/build-mock-start-change.ts +38 -0
- package/src/test-utils/claim-store-attempt-reader-cases.ts +357 -0
- package/src/test-utils/claim-store-claim-cases.ts +314 -0
- package/src/test-utils/claim-store-close-cases.ts +95 -0
- package/src/test-utils/claim-store-contract.ts +28 -0
- package/src/test-utils/claim-store-copy-cases.ts +100 -0
- package/src/test-utils/claim-store-exclusive-cases.ts +243 -0
- package/src/test-utils/claim-store-guard-cases.ts +383 -0
- package/src/test-utils/claim-store-item-reader-cases.ts +471 -0
- package/src/test-utils/claim-store-outbox-cases.ts +281 -0
- package/src/test-utils/claim-store-race-cases.ts +150 -0
- package/src/test-utils/claim-store-rollback-cases.ts +175 -0
- package/src/test-utils/claim-store-seat-cases.ts +270 -0
- package/src/test-utils/claim-store-session-cases.ts +289 -0
- package/src/test-utils/collect-memory-limit-fills.ts +15 -0
- package/src/test-utils/compare-text.ts +8 -0
- package/src/test-utils/create-fake-backoff.ts +16 -0
- package/src/test-utils/create-fake-clock.ts +20 -0
- package/src/test-utils/create-fake-fact-sink.ts +74 -0
- package/src/test-utils/create-fake-forge.ts +117 -0
- package/src/test-utils/create-fake-worker.ts +303 -0
- package/src/test-utils/create-memory-claim-store.ts +163 -0
- package/src/test-utils/create-test-dispatch.ts +597 -0
- package/src/test-utils/forge-contract.ts +171 -0
- package/src/test-utils/is-same-held-seat.ts +11 -0
- package/src/test-utils/types.ts +23 -0
- package/src/test-utils/worker-contract.ts +192 -0
- package/src/timeouts.ts +17 -0
- package/src/types.ts +32 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AttemptId, SpawnRequest, WorkerKind } from '@boxxy/model';
|
|
2
|
+
export interface WorkerHarness {
|
|
3
|
+
readonly worker: WorkerKind;
|
|
4
|
+
buildSpawnRequest: (attempt: AttemptId) => SpawnRequest;
|
|
5
|
+
}
|
|
6
|
+
export interface WorkerContractOutcome {
|
|
7
|
+
readonly received: unknown;
|
|
8
|
+
readonly expected: unknown;
|
|
9
|
+
}
|
|
10
|
+
export type WorkerContractCase = readonly [
|
|
11
|
+
title: string,
|
|
12
|
+
check: (harness: WorkerHarness) => Promise<WorkerContractOutcome>
|
|
13
|
+
];
|
|
14
|
+
export declare const workerContract: readonly WorkerContractCase[];
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DispatchTimeouts {
|
|
2
|
+
readonly spawnLostMinutes: number;
|
|
3
|
+
readonly resumeLostMinutes: number;
|
|
4
|
+
readonly closingGiveUpMinutes: number;
|
|
5
|
+
readonly stopGraceSeconds: number;
|
|
6
|
+
readonly seatlessSeconds: number;
|
|
7
|
+
readonly capacityBackoffSeconds: number;
|
|
8
|
+
readonly stallMinutes: number;
|
|
9
|
+
readonly keptSessionHours: number;
|
|
10
|
+
readonly failedSessionHours: number;
|
|
11
|
+
readonly usageLimitBackoffHours: number;
|
|
12
|
+
readonly strayHours: number;
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boxxy/dispatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "boxxy's dispatch: claims the next task for an agent",
|
|
5
5
|
"homepage": "https://github.com/zgeoff/boxxy/tree/main/modules/dispatch#readme",
|
|
6
6
|
"bugs": "https://github.com/zgeoff/boxxy/issues",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
".": {
|
|
21
21
|
"types": "./dist/index.d.ts",
|
|
22
22
|
"default": "./src/index.ts"
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"./test-utils/*": "./src/test-utils/*.ts"
|
|
24
25
|
},
|
|
25
26
|
"publishConfig": {
|
|
26
27
|
"access": "public"
|
|
@@ -29,6 +30,18 @@
|
|
|
29
30
|
"build": "rm -rf dist && tsrs -p tsconfig.build.json",
|
|
30
31
|
"typecheck": "tsrs --noEmit"
|
|
31
32
|
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@boxxy/closeout": "0.1.3",
|
|
35
|
+
"@boxxy/content-hash": "0.1.3",
|
|
36
|
+
"@boxxy/model": "0.1.3",
|
|
37
|
+
"@boxxy/planning": "0.1.3",
|
|
38
|
+
"tiny-invariant": "1.3.3",
|
|
39
|
+
"ts-pattern": "5.9.2",
|
|
40
|
+
"xstate": "6.0.0-beta.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@boxxy/rules": "0.1.3"
|
|
44
|
+
},
|
|
32
45
|
"engines": {
|
|
33
46
|
"bun": ">=1.3",
|
|
34
47
|
"node": ">=24"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { AttemptState, SessionMode } from '@boxxy/model';
|
|
2
|
+
import invariant from 'tiny-invariant';
|
|
3
|
+
import { createMachine, types } from 'xstate';
|
|
4
|
+
import { attemptMoves } from './attempt-moves.ts';
|
|
5
|
+
import type { AttemptContext, AttemptMachineEvent, AttemptMove, MoveEffect } from './types.ts';
|
|
6
|
+
|
|
7
|
+
// The attempt states as an XState machine, built from the table of moves. Callers run it only
|
|
8
|
+
// through the pure `transition` and `initialTransition`, and read each move's writes from the
|
|
9
|
+
// `effect` events it emits.
|
|
10
|
+
export const attemptMachine = createMachine({
|
|
11
|
+
schemas: {
|
|
12
|
+
context: types<AttemptContext>(),
|
|
13
|
+
input: types<{ mode: SessionMode }>(),
|
|
14
|
+
events: {
|
|
15
|
+
started: types<{ via: 'start' | 'reconcile' }>(),
|
|
16
|
+
'spawn-unanswered': types<void>(),
|
|
17
|
+
'session-seen': types<void>(),
|
|
18
|
+
'closed-out': types<void>(),
|
|
19
|
+
'timed-out': types<void>(),
|
|
20
|
+
'item-closed': types<void>(),
|
|
21
|
+
'turn-ended': types<void>(),
|
|
22
|
+
stalled: types<void>(),
|
|
23
|
+
'skill-missing': types<void>(),
|
|
24
|
+
'usage-limited': types<void>(),
|
|
25
|
+
'suspend-asked': types<void>(),
|
|
26
|
+
'suspend-refused': types<void>(),
|
|
27
|
+
slept: types<void>(),
|
|
28
|
+
'wake-requested': types<void>(),
|
|
29
|
+
woken: types<{ overCap: boolean }>(),
|
|
30
|
+
resumed: types<void>(),
|
|
31
|
+
'wake-refused': types<void>(),
|
|
32
|
+
settled: types<{ state: 'succeeded' | 'failed'; sessionStopped: boolean }>(),
|
|
33
|
+
'stray-stopped': types<void>(),
|
|
34
|
+
'session-forgotten': types<void>(),
|
|
35
|
+
},
|
|
36
|
+
emitted: { effect: types<{ effect: MoveEffect }>() },
|
|
37
|
+
},
|
|
38
|
+
context: (args) => ({
|
|
39
|
+
mark: false,
|
|
40
|
+
wakeRequested: false,
|
|
41
|
+
mode: args.input.mode,
|
|
42
|
+
holdGen: 1,
|
|
43
|
+
closing: null,
|
|
44
|
+
hasSession: false,
|
|
45
|
+
strayed: false,
|
|
46
|
+
sessionStopped: false,
|
|
47
|
+
sessionForgotten: false,
|
|
48
|
+
}),
|
|
49
|
+
entry: (_args, enq) => {
|
|
50
|
+
for (const effect of getClaimedMove().effects) {
|
|
51
|
+
enq.emit({ type: 'effect', effect });
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
initial: 'starting',
|
|
55
|
+
states: {
|
|
56
|
+
starting: { on: buildStateHandlers('starting') },
|
|
57
|
+
reconciling: { on: buildStateHandlers('reconciling') },
|
|
58
|
+
running: { on: buildStateHandlers('running') },
|
|
59
|
+
sleeping: { on: buildStateHandlers('sleeping') },
|
|
60
|
+
waking: { on: buildStateHandlers('waking') },
|
|
61
|
+
closing: { on: buildStateHandlers('closing') },
|
|
62
|
+
|
|
63
|
+
// plain states, not final: a final state drops `session-seen` and `stray-stopped`
|
|
64
|
+
succeeded: { on: buildStateHandlers('succeeded') },
|
|
65
|
+
failed: { on: buildStateHandlers('failed') },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function getClaimedMove(): AttemptMove {
|
|
70
|
+
const claimed = attemptMoves.find((move) => move.event === 'claimed');
|
|
71
|
+
|
|
72
|
+
invariant(claimed !== undefined, 'the table of moves has a claimed row');
|
|
73
|
+
|
|
74
|
+
return claimed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface MoveArgs {
|
|
78
|
+
readonly context: AttemptContext;
|
|
79
|
+
readonly event: AttemptMachineEvent;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface EffectEvent {
|
|
83
|
+
type: 'effect';
|
|
84
|
+
effect: MoveEffect;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface MoveEnqueue {
|
|
88
|
+
readonly emit: (event: EffectEvent) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface MoveTarget {
|
|
92
|
+
target?: AttemptState;
|
|
93
|
+
context?: Partial<AttemptContext>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type MoveHandler = (args: MoveArgs, enq: MoveEnqueue) => MoveTarget | undefined;
|
|
97
|
+
|
|
98
|
+
// Every state lists every event, and a handler with no matching row leaves its event unhandled.
|
|
99
|
+
// The type check of `createMachine` refuses an `on` object whose keys are optional.
|
|
100
|
+
function buildStateHandlers(state: AttemptState): Record<AttemptMachineEvent['type'], MoveHandler> {
|
|
101
|
+
const build = (event: AttemptMachineEvent['type']): MoveHandler =>
|
|
102
|
+
buildMoveHandler(
|
|
103
|
+
attemptMoves.filter((move) => move.event === event && move.from.includes(state)),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
started: build('started'),
|
|
108
|
+
'spawn-unanswered': build('spawn-unanswered'),
|
|
109
|
+
'session-seen': build('session-seen'),
|
|
110
|
+
'closed-out': build('closed-out'),
|
|
111
|
+
'timed-out': build('timed-out'),
|
|
112
|
+
'item-closed': build('item-closed'),
|
|
113
|
+
'turn-ended': build('turn-ended'),
|
|
114
|
+
stalled: build('stalled'),
|
|
115
|
+
'skill-missing': build('skill-missing'),
|
|
116
|
+
'usage-limited': build('usage-limited'),
|
|
117
|
+
'suspend-asked': build('suspend-asked'),
|
|
118
|
+
'suspend-refused': build('suspend-refused'),
|
|
119
|
+
slept: build('slept'),
|
|
120
|
+
'wake-requested': build('wake-requested'),
|
|
121
|
+
woken: build('woken'),
|
|
122
|
+
resumed: build('resumed'),
|
|
123
|
+
'wake-refused': build('wake-refused'),
|
|
124
|
+
settled: build('settled'),
|
|
125
|
+
'stray-stopped': build('stray-stopped'),
|
|
126
|
+
'session-forgotten': build('session-forgotten'),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildMoveHandler(moves: readonly AttemptMove[]): MoveHandler {
|
|
131
|
+
return (args, enq) => {
|
|
132
|
+
const move = moves.find((candidate) => candidate.when?.(args.context, args.event) ?? true);
|
|
133
|
+
|
|
134
|
+
// XState counts any queued effect as a taken move, so the guard runs before the effects
|
|
135
|
+
return move === undefined ? undefined : takeMove(move, args, enq);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function takeMove(move: AttemptMove, args: MoveArgs, enq: MoveEnqueue): MoveTarget {
|
|
140
|
+
for (const effect of move.effects) {
|
|
141
|
+
enq.emit({ type: 'effect', effect });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const patch = move.context?.(args.context, args.event) ?? {};
|
|
145
|
+
|
|
146
|
+
return move.to === 'same' ? { context: patch } : { target: move.to, context: patch };
|
|
147
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { AttemptState } from '@boxxy/model';
|
|
2
|
+
import type { AttemptMove } from './types.ts';
|
|
3
|
+
|
|
4
|
+
const liveStates: readonly AttemptState[] = [
|
|
5
|
+
'starting',
|
|
6
|
+
'reconciling',
|
|
7
|
+
'running',
|
|
8
|
+
'sleeping',
|
|
9
|
+
'waking',
|
|
10
|
+
'closing',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// The table of section A of the core flow sketch. The attempt machine is built from it, and the
|
|
14
|
+
// first row whose `from` and `when` match an event is the move.
|
|
15
|
+
export const attemptMoves: readonly AttemptMove[] = [
|
|
16
|
+
{
|
|
17
|
+
event: 'claimed',
|
|
18
|
+
from: [],
|
|
19
|
+
to: 'starting',
|
|
20
|
+
effects: [
|
|
21
|
+
'take-lease',
|
|
22
|
+
'take-seats',
|
|
23
|
+
'record-decision',
|
|
24
|
+
'boxxy.attempt.claimed',
|
|
25
|
+
'boxxy.decision.recorded',
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
event: 'started',
|
|
30
|
+
from: ['starting', 'reconciling'],
|
|
31
|
+
when: (_context, event) => event.type === 'started' && event.via === 'start',
|
|
32
|
+
to: 'running',
|
|
33
|
+
effects: ['boxxy.attempt.started'],
|
|
34
|
+
context: () => ({ hasSession: true }),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
event: 'started',
|
|
38
|
+
from: ['starting', 'reconciling'],
|
|
39
|
+
when: (_context, event) => event.type === 'started' && event.via === 'reconcile',
|
|
40
|
+
to: 'running',
|
|
41
|
+
effects: ['boxxy.attempt.reconciled'],
|
|
42
|
+
context: () => ({ hasSession: true }),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
event: 'started',
|
|
46
|
+
from: ['closing'],
|
|
47
|
+
when: (context) => context.closing === 'item-closed' && !context.hasSession,
|
|
48
|
+
to: 'same',
|
|
49
|
+
effects: ['boxxy.attempt.started'],
|
|
50
|
+
context: () => ({ hasSession: true }),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
event: 'spawn-unanswered',
|
|
54
|
+
from: ['starting'],
|
|
55
|
+
to: 'reconciling',
|
|
56
|
+
effects: ['boxxy.attempt.spawn_unanswered'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
event: 'session-seen',
|
|
60
|
+
from: [...liveStates, 'succeeded', 'failed'],
|
|
61
|
+
to: 'same',
|
|
62
|
+
effects: ['bind-session', 'boxxy.attempt.session_bound'],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
event: 'closed-out',
|
|
66
|
+
from: ['starting', 'reconciling', 'running', 'sleeping', 'waking'],
|
|
67
|
+
to: 'closing',
|
|
68
|
+
effects: ['boxxy.attempt.closed_out'],
|
|
69
|
+
context: () => ({ closing: 'closed-out', mark: false, wakeRequested: false }),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
event: 'timed-out',
|
|
73
|
+
from: ['running'],
|
|
74
|
+
when: (context) => context.mode === 'agent',
|
|
75
|
+
to: 'closing',
|
|
76
|
+
effects: ['boxxy.attempt.timed_out'],
|
|
77
|
+
context: () => ({ closing: 'timed-out' }),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
// the close of the item writes `boxxy.item.closed` for each attempt it ends
|
|
81
|
+
event: 'item-closed',
|
|
82
|
+
from: ['starting', 'reconciling', 'running', 'waking'],
|
|
83
|
+
to: 'closing',
|
|
84
|
+
effects: [],
|
|
85
|
+
context: () => ({ closing: 'item-closed', mark: false, wakeRequested: false }),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
event: 'turn-ended',
|
|
89
|
+
from: ['running'],
|
|
90
|
+
when: (context) => context.mode === 'agent',
|
|
91
|
+
to: 'closing',
|
|
92
|
+
effects: ['boxxy.attempt.turn_ended'],
|
|
93
|
+
context: () => ({ closing: 'turn-ended' }),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
// `boxxy.attempt.settled` carries the cause
|
|
97
|
+
event: 'stalled',
|
|
98
|
+
from: ['running'],
|
|
99
|
+
when: (context) => context.mode === 'agent',
|
|
100
|
+
to: 'closing',
|
|
101
|
+
effects: [],
|
|
102
|
+
context: () => ({ closing: 'stalled' }),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
// `boxxy.attempt.settled` carries the cause
|
|
106
|
+
event: 'skill-missing',
|
|
107
|
+
from: ['starting', 'reconciling', 'running'],
|
|
108
|
+
to: 'closing',
|
|
109
|
+
effects: [],
|
|
110
|
+
context: () => ({ closing: 'skill-missing' }),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
// `boxxy.attempt.settled` carries the cause
|
|
114
|
+
event: 'usage-limited',
|
|
115
|
+
from: ['running'],
|
|
116
|
+
to: 'closing',
|
|
117
|
+
effects: [],
|
|
118
|
+
context: () => ({ closing: 'usage-limit' }),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
event: 'suspend-asked',
|
|
122
|
+
from: ['running'],
|
|
123
|
+
when: (context) => !context.mark,
|
|
124
|
+
to: 'same',
|
|
125
|
+
effects: [],
|
|
126
|
+
context: () => ({ mark: true }),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
event: 'suspend-refused',
|
|
130
|
+
from: ['running'],
|
|
131
|
+
when: (context) => context.mark,
|
|
132
|
+
to: 'same',
|
|
133
|
+
effects: [],
|
|
134
|
+
context: () => ({ mark: false, wakeRequested: false }),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
event: 'slept',
|
|
138
|
+
from: ['starting', 'reconciling', 'running'],
|
|
139
|
+
to: 'sleeping',
|
|
140
|
+
effects: ['release-seats', 'boxxy.attempt.slept'],
|
|
141
|
+
context: () => ({ mark: false }),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
event: 'wake-requested',
|
|
145
|
+
from: ['sleeping'],
|
|
146
|
+
when: (context) => !context.wakeRequested,
|
|
147
|
+
to: 'same',
|
|
148
|
+
effects: ['boxxy.attempt.wake_requested'],
|
|
149
|
+
context: () => ({ wakeRequested: true }),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
event: 'wake-requested',
|
|
153
|
+
from: ['running'],
|
|
154
|
+
when: (context) => context.mark,
|
|
155
|
+
to: 'same',
|
|
156
|
+
effects: ['boxxy.attempt.wake_requested'],
|
|
157
|
+
context: () => ({ wakeRequested: true }),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
event: 'woken',
|
|
161
|
+
from: ['sleeping'],
|
|
162
|
+
to: 'waking',
|
|
163
|
+
effects: ['take-seats', 'record-decision', 'boxxy.attempt.woken', 'boxxy.decision.recorded'],
|
|
164
|
+
context: (context) => ({ holdGen: context.holdGen + 1 }),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
// the wake is done, so a later sleep starts with no request
|
|
168
|
+
event: 'resumed',
|
|
169
|
+
from: ['waking'],
|
|
170
|
+
to: 'running',
|
|
171
|
+
effects: ['boxxy.attempt.resumed'],
|
|
172
|
+
context: () => ({ wakeRequested: false }),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
// the request stays, so a later claim tries the wake again
|
|
176
|
+
event: 'wake-refused',
|
|
177
|
+
from: ['waking'],
|
|
178
|
+
to: 'sleeping',
|
|
179
|
+
effects: ['release-seats', 'boxxy.attempt.slept'],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
event: 'settled',
|
|
183
|
+
from: liveStates,
|
|
184
|
+
when: (_context, event) => event.type === 'settled' && event.state === 'succeeded',
|
|
185
|
+
to: 'succeeded',
|
|
186
|
+
effects: ['release-seats', 'release-lease', 'boxxy.attempt.settled'],
|
|
187
|
+
context: (_context, event) => ({
|
|
188
|
+
strayed: event.type === 'settled' && !event.sessionStopped,
|
|
189
|
+
}),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
event: 'settled',
|
|
193
|
+
from: liveStates,
|
|
194
|
+
when: (_context, event) => event.type === 'settled' && event.state === 'failed',
|
|
195
|
+
to: 'failed',
|
|
196
|
+
effects: ['release-seats', 'release-lease', 'boxxy.attempt.settled'],
|
|
197
|
+
context: (_context, event) => ({
|
|
198
|
+
strayed: event.type === 'settled' && !event.sessionStopped,
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
event: 'stray-stopped',
|
|
203
|
+
from: ['succeeded', 'failed'],
|
|
204
|
+
when: (context) => context.strayed && !context.sessionStopped,
|
|
205
|
+
to: 'same',
|
|
206
|
+
effects: [],
|
|
207
|
+
context: () => ({ sessionStopped: true }),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
// a stray session is forgotten only after it has stopped
|
|
211
|
+
event: 'session-forgotten',
|
|
212
|
+
from: ['succeeded', 'failed'],
|
|
213
|
+
when: (context) => !context.sessionForgotten && (!context.strayed || context.sessionStopped),
|
|
214
|
+
to: 'same',
|
|
215
|
+
effects: [],
|
|
216
|
+
context: () => ({ sessionForgotten: true }),
|
|
217
|
+
},
|
|
218
|
+
];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentSessionId,
|
|
3
|
+
ClaimStore,
|
|
4
|
+
Instant,
|
|
5
|
+
NewId,
|
|
6
|
+
SessionFault,
|
|
7
|
+
StoreTx,
|
|
8
|
+
} from '@boxxy/model';
|
|
9
|
+
import type { CallRef } from '../closeouts/types.ts';
|
|
10
|
+
import { pickCallAttempt } from './pick-call-attempt.ts';
|
|
11
|
+
import { writeAttemptMove } from './write-attempt-move.ts';
|
|
12
|
+
import { writeSessionBind } from './write-session-bind.ts';
|
|
13
|
+
|
|
14
|
+
export interface BindAttemptSessionInput {
|
|
15
|
+
readonly store: ClaimStore;
|
|
16
|
+
readonly call: CallRef;
|
|
17
|
+
readonly agentSession: AgentSessionId;
|
|
18
|
+
|
|
19
|
+
// each skill the recipe lists that the session-start hook did not find after its fetch
|
|
20
|
+
readonly missingSkills: readonly string[];
|
|
21
|
+
readonly now: Instant;
|
|
22
|
+
readonly newId: NewId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type BindAnswer =
|
|
26
|
+
| { readonly kind: 'bound' | 'no-attempt' | 'not-current' | 'skill-missing' }
|
|
27
|
+
| SessionFault;
|
|
28
|
+
|
|
29
|
+
// The session-start hook's call. It reaches only the attempt that `pickCallAttempt` gives. A
|
|
30
|
+
// missing skill moves the attempt to `closing`, and the runner settles it as `skill-missing`.
|
|
31
|
+
export function bindAttemptSession(input: BindAttemptSessionInput): Promise<BindAnswer> {
|
|
32
|
+
return input.store.exclusive((tx) => writeBind(tx, input));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function writeBind(tx: StoreTx, input: BindAttemptSessionInput): Promise<BindAnswer> {
|
|
36
|
+
const latest = await tx.findAttemptBySession(input.call.session);
|
|
37
|
+
|
|
38
|
+
const picked = pickCallAttempt(latest, input.call);
|
|
39
|
+
|
|
40
|
+
if (picked.kind === 'refused') {
|
|
41
|
+
return { kind: picked.reason };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const attempt = picked.attempt;
|
|
45
|
+
const base = { actor: `boxxy:hook:${attempt.id}`, now: input.now, newId: input.newId } as const;
|
|
46
|
+
|
|
47
|
+
if (input.missingSkills.length > 0) {
|
|
48
|
+
await writeAttemptMove(tx, attempt.id, { kind: 'skill-missing' }, base);
|
|
49
|
+
|
|
50
|
+
return { kind: 'skill-missing' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fault = await writeSessionBind(
|
|
54
|
+
tx,
|
|
55
|
+
attempt,
|
|
56
|
+
{ atcSession: input.call.session, agentSession: input.agentSession },
|
|
57
|
+
base,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return fault ?? { kind: 'bound' };
|
|
61
|
+
}
|