@abide/abide 0.123.1 → 0.123.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/CHANGELOG.md +33 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +2 -2
- package/docs/RULEBOOK.md +2 -2
- package/docs/abide.md +2 -2
- package/package.json +1 -1
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/Lowering.ts +1 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/TypeSession.ts +62 -85
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/compileApp.ts +101 -70
- package/src/compiler/declaredExports.ts +2 -4
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +2 -2
- package/src/compiler/handlersUnder.ts +17 -29
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +1 -1
- package/src/compiler/serveLanguage.ts +23 -57
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/answerRequest.ts +11 -300
- package/src/server/answerRpc.ts +12 -54
- package/src/server/answerSocket.ts +8 -35
- package/src/server/appBuild.ts +377 -0
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +30 -64
- package/src/server/devWorker.ts +11 -15
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +2 -2
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +3 -2
- package/src/server/principal.ts +4 -3
- package/src/server/remote.ts +1 -1
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +1 -1
- package/src/server/seedBuffer.ts +1 -1
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +2 -1
- package/src/server/versionOf.ts +6 -15
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +1 -1
- package/src/shared/reactive/Reactive.ts +21 -8
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/index.ts +1 -0
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/rpc.ts +12 -19
- package/src/shared/validateJson.ts +14 -3
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +54 -245
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +21 -3
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +26 -11
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -95
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- package/src/shared/reactive/canonicalKey.ts +0 -95
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// WHAT A CALL'S ARGS ARE BEFORE ITS HANDLER SEES THEM, on a server: the schema a handler's args are
|
|
2
|
+
// read by, out of the sources it has, and the args a call off the wire is answered with — over http
|
|
3
|
+
// (16.2) and a `GET` followed over the socket mux (18.3). Each lane reads the args off its own
|
|
4
|
+
// transport and answers a refusal in its own form; what every way of calling a handler shares, in
|
|
5
|
+
// process too, is `acceptedArgs` (`#shared/reactive/argsContract.ts`), which this ends in.
|
|
6
|
+
//
|
|
7
|
+
// 19.7 — A URL CARRIES TEXT, so `?id=42` is `'42'` until the schema says the member is a number.
|
|
8
|
+
// Coercion is the pipeline's and not the schema's (19.6): the schema is handed what this made of the
|
|
9
|
+
// text, and refuses what would not coerce. A schema with no document has declared no types, and its
|
|
10
|
+
// args stay the text they arrived as.
|
|
11
|
+
|
|
12
|
+
import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
|
|
13
|
+
import { isRecord } from '#shared/isRecord.ts'
|
|
14
|
+
import type { JsonValue } from '#shared/JsonValue.ts'
|
|
15
|
+
import { numberFromText } from '#shared/numberFromText.ts'
|
|
16
|
+
import { bareName } from '#shared/queryText.ts'
|
|
17
|
+
import {
|
|
18
|
+
acceptedArgs,
|
|
19
|
+
type Gated,
|
|
20
|
+
gateOf,
|
|
21
|
+
type HeldToAnnotation,
|
|
22
|
+
} from '#shared/reactive/argsContract.ts'
|
|
23
|
+
import { refuse } from '#shared/refuse.ts'
|
|
24
|
+
import { validateJson } from '#shared/validateJson.ts'
|
|
25
|
+
|
|
26
|
+
import type { Mounted } from './answerRpc.ts'
|
|
27
|
+
|
|
28
|
+
export type Document = {
|
|
29
|
+
type?: string | string[]
|
|
30
|
+
properties?: Record<string, Document>
|
|
31
|
+
items?: Document
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Each handler's document, read once: 19.7, 20.11 — a third-party schema converts itself on every
|
|
35
|
+
// probe. 19.9 — the one its annotation derives is filed here by `derivedArgs`.
|
|
36
|
+
const DOCUMENTS = new WeakMap<object, Document | undefined>()
|
|
37
|
+
|
|
38
|
+
const NO_PARAMETERS: Record<string, string | string[]> = {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The document a handler's args are read by: the one its `args` schema declares, or else the one the
|
|
42
|
+
* build derived off its annotation (`derivedArgs`), or `undefined`.
|
|
43
|
+
*/
|
|
44
|
+
export function argsDocumentOf(handler: Gated<unknown>): Document | undefined {
|
|
45
|
+
if (DOCUMENTS.has(handler)) return DOCUMENTS.get(handler)
|
|
46
|
+
const document = declaredDocument(handler)
|
|
47
|
+
DOCUMENTS.set(handler, document)
|
|
48
|
+
return document
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function declaredDocument(handler: Gated<unknown>): Document | undefined {
|
|
52
|
+
const gate = gateOf(handler) as
|
|
53
|
+
{ '~standard'?: { jsonSchema?: { input?: () => unknown } } } | undefined
|
|
54
|
+
const document = gate?.['~standard']?.jsonSchema?.input?.()
|
|
55
|
+
return document !== null && typeof document === 'object'
|
|
56
|
+
? (document as Document)
|
|
57
|
+
: undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Holds `handler`'s args to `schema`, the one the build derived off its annotation, for every call
|
|
62
|
+
* of it from now on: in process and over the wire (15.8, 19.9).
|
|
63
|
+
*
|
|
64
|
+
* What the module a handler is loaded through calls once its handler is built — a handler file's
|
|
65
|
+
* face and a `<script server>`'s module — so the schema is there wherever the handler can be
|
|
66
|
+
* called, whether or not an app mounted it. Not where its `args` gate publishes a document of its
|
|
67
|
+
* own, which holds the args itself (19.7). Args that did not come are held as an empty record: a
|
|
68
|
+
* schema is only derived where the handler's args are a required record, so a required member
|
|
69
|
+
* missing is refused, not read off nothing.
|
|
70
|
+
*/
|
|
71
|
+
export function derivedArgs(
|
|
72
|
+
handler: HeldToAnnotation & Gated<unknown>,
|
|
73
|
+
schema: JsonValue,
|
|
74
|
+
): void {
|
|
75
|
+
// Held already, by the module it loaded through, to the schema its row names as well.
|
|
76
|
+
if (handler[DERIVED_ARGS] !== undefined) return
|
|
77
|
+
const declared = declaredDocument(handler)
|
|
78
|
+
DOCUMENTS.set(handler, declared ?? (schema as Document))
|
|
79
|
+
if (declared === undefined)
|
|
80
|
+
handler[DERIVED_ARGS] = (args) => validateJson(schema, args ?? {})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Name and value pairs as args: a name given more than once is a list, one given once its text.
|
|
85
|
+
*
|
|
86
|
+
* 16.18 — a query's and a form's members, and 38.61 — `abide call`'s `name=value` words, alike.
|
|
87
|
+
*/
|
|
88
|
+
export function collapsed(
|
|
89
|
+
entries: URLSearchParams | FormData,
|
|
90
|
+
): Record<string, string | string[]> {
|
|
91
|
+
const args: Record<string, string | string[]> = {}
|
|
92
|
+
for (const name of new Set(entries.keys())) {
|
|
93
|
+
const values = entries.getAll(name) as string[]
|
|
94
|
+
args[name] = values.length === 1 ? (values[0] as string) : values
|
|
95
|
+
}
|
|
96
|
+
return args
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The args a call over the wire is answered with, out of the args it sent: the parameters its route
|
|
101
|
+
* matched laid in, the text a URL or a form carried coerced where `query` says the args are text,
|
|
102
|
+
* then `acceptedArgs`. Throws the refusal where they are not accepted.
|
|
103
|
+
*
|
|
104
|
+
* `query` is the raw `?…` the args were read from — `''` for a form, `undefined` for JSON.
|
|
105
|
+
* 16.65 — the path's parameters, whatever the method. 16.66 — one the query or the body names as well
|
|
106
|
+
* is refused rather than one of the two chosen. A JSON body's members are typed already, so the
|
|
107
|
+
* path's text is coerced to the declared types before it joins them (19.7); text args are coerced
|
|
108
|
+
* with the rest of the text.
|
|
109
|
+
*/
|
|
110
|
+
export function argsFor(
|
|
111
|
+
handler: Mounted['handler'],
|
|
112
|
+
sent: unknown,
|
|
113
|
+
query: string | undefined,
|
|
114
|
+
parameters: Record<string, string | string[]> = NO_PARAMETERS,
|
|
115
|
+
): unknown {
|
|
116
|
+
const document = argsDocumentOf(handler)
|
|
117
|
+
let args = sent
|
|
118
|
+
let none = true
|
|
119
|
+
for (const name in parameters) {
|
|
120
|
+
none = false
|
|
121
|
+
if (isRecord(sent) && name in sent)
|
|
122
|
+
throw refuse(
|
|
123
|
+
400,
|
|
124
|
+
`The parameter '${name}' is in the path and in the request as well.`,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
if (!none) {
|
|
128
|
+
const fromPath =
|
|
129
|
+
query === undefined
|
|
130
|
+
? (coerceArgs(parameters, document, '') as Record<
|
|
131
|
+
string,
|
|
132
|
+
unknown
|
|
133
|
+
>)
|
|
134
|
+
: parameters
|
|
135
|
+
args = isRecord(sent) ? { ...sent, ...fromPath } : { ...fromPath }
|
|
136
|
+
}
|
|
137
|
+
if (query !== undefined)
|
|
138
|
+
args = coerceArgs(
|
|
139
|
+
args as Record<string, string | string[]> | undefined,
|
|
140
|
+
document,
|
|
141
|
+
query,
|
|
142
|
+
)
|
|
143
|
+
return acceptedArgs(handler, args)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The text args, each member coerced to the type `document` declares.
|
|
148
|
+
*
|
|
149
|
+
* `URLSearchParams` reads `rpcRequest`'s empty list, a bare name, as `''` — the same as `tags=`, the
|
|
150
|
+
* list `['']` — so a declared array tells the two apart by `query`. A text member keeps the `''`.
|
|
151
|
+
*/
|
|
152
|
+
function coerceArgs(
|
|
153
|
+
args: Record<string, string | string[]> | undefined,
|
|
154
|
+
document: Document | undefined,
|
|
155
|
+
query: string,
|
|
156
|
+
): unknown {
|
|
157
|
+
if (args === undefined || document?.properties === undefined) return args
|
|
158
|
+
const coerced: Record<string, unknown> = {}
|
|
159
|
+
for (const [name, text] of Object.entries(args)) {
|
|
160
|
+
const member = document.properties[name]
|
|
161
|
+
if (member === undefined) coerced[name] = text
|
|
162
|
+
else if (
|
|
163
|
+
text === '' &&
|
|
164
|
+
typesOf(member).includes('array') &&
|
|
165
|
+
`&${query.slice(1)}&`.includes(`&${bareName(name)}&`)
|
|
166
|
+
)
|
|
167
|
+
coerced[name] = []
|
|
168
|
+
else coerced[name] = coerceMember(text, member)
|
|
169
|
+
}
|
|
170
|
+
return coerced
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function typesOf(member: Document): string[] {
|
|
174
|
+
if (member.type === undefined) return []
|
|
175
|
+
return Array.isArray(member.type) ? member.type : [member.type]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function coerceMember(text: string | string[], member: Document): unknown {
|
|
179
|
+
const types = typesOf(member)
|
|
180
|
+
// A list is its member repeated, and one given once still answers a declared array.
|
|
181
|
+
if (types.includes('array')) {
|
|
182
|
+
const items = Array.isArray(text) ? text : [text]
|
|
183
|
+
const item = member.items
|
|
184
|
+
return item === undefined
|
|
185
|
+
? items
|
|
186
|
+
: items.map((each) => coerceScalar(each, item))
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(text)) return text
|
|
189
|
+
return coerceScalar(text, member)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The text as the first declared type it reads as, in the order the schema lists them.
|
|
194
|
+
*
|
|
195
|
+
* Text that reads as none stays text, for the schema to refuse.
|
|
196
|
+
*/
|
|
197
|
+
function coerceScalar(text: string, member: Document): unknown {
|
|
198
|
+
for (const type of typesOf(member)) {
|
|
199
|
+
if (type === 'string') return text
|
|
200
|
+
if (type === 'number' || type === 'integer') {
|
|
201
|
+
const number = numberFromText(text)
|
|
202
|
+
if (number !== undefined) return number
|
|
203
|
+
}
|
|
204
|
+
if (type === 'boolean' && (text === 'true' || text === 'false'))
|
|
205
|
+
return text === 'true'
|
|
206
|
+
if (type === 'null' && text === 'null') return null
|
|
207
|
+
}
|
|
208
|
+
return text
|
|
209
|
+
}
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
// in there when its code was minted (44.33), for as long as the bridge's stream is open, or
|
|
13
13
|
// resumable. HELD IN THIS PROCESS: an app served by several needs a visitor's requests to reach the
|
|
14
14
|
// one its bridge dialed.
|
|
15
|
+
//
|
|
16
|
+
// Every frame on that stream is written here; what the rest of the server sees is a turn run, a
|
|
17
|
+
// pairing's status as it changes, and a pairing code.
|
|
15
18
|
|
|
16
19
|
import { isFailed } from '#shared/Failed.ts'
|
|
17
20
|
import { refuse } from '#shared/refuse.ts'
|
|
@@ -19,14 +22,17 @@ import { refuse } from '#shared/refuse.ts'
|
|
|
19
22
|
import { answered } from './answerFailure.ts'
|
|
20
23
|
import { bodyWithin } from './answerRpc.ts'
|
|
21
24
|
import { framed } from './answerValue.ts'
|
|
25
|
+
import type { AssistantEvent } from './assistant.ts'
|
|
22
26
|
import { Inbox } from './Inbox.ts'
|
|
23
27
|
import { randomToken } from './randomToken.ts'
|
|
28
|
+
import { RESUME_MILLISECONDS } from './RESUME_MILLISECONDS.ts'
|
|
29
|
+
import { onSignInChanged } from './signInChanged.ts'
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
32
|
* What a turn hears from the bridge, in the order the bridge posted it: an event, an MCP request, the
|
|
27
33
|
* end, or a failure. ONE SHAPE for all four, every field set, since one is built per streamed chunk.
|
|
28
34
|
*/
|
|
29
|
-
|
|
35
|
+
type TurnItem = {
|
|
30
36
|
kind: 'event' | 'mcp' | 'end' | 'fail'
|
|
31
37
|
event: unknown
|
|
32
38
|
id: number
|
|
@@ -36,7 +42,7 @@ export type TurnItem = {
|
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/** One paired bridge: what it is sent, and the turns it is answering. */
|
|
39
|
-
|
|
45
|
+
type Bridge = {
|
|
40
46
|
/** The browser and the sign-in it is paired to (`pairingKey`). 44.33. */
|
|
41
47
|
key: string
|
|
42
48
|
token: string
|
|
@@ -53,8 +59,6 @@ export type Bridge = {
|
|
|
53
59
|
const CODE_LIFETIME = 10 * 60_000
|
|
54
60
|
// How long a bridge outlives the last page watching it, so a reload does not end it.
|
|
55
61
|
const IDLE_GRACE = 30_000
|
|
56
|
-
/** How long a pairing whose stream dropped waits to be resumed. 44.26. */
|
|
57
|
-
export const RESUME_MILLISECONDS = 60_000
|
|
58
62
|
|
|
59
63
|
const CODES = new Map<string, { key: string; expires: number }>()
|
|
60
64
|
const BY_KEY = new Map<string, Bridge>()
|
|
@@ -65,6 +69,13 @@ const FOLLOWERS = new Map<
|
|
|
65
69
|
Set<(caller: string, signedIn: string) => void>
|
|
66
70
|
>()
|
|
67
71
|
|
|
72
|
+
// 44.34 — A STATUS STREAM OPENED BEFORE A SIGN-IN carries the cookies it was opened with, so it hears
|
|
73
|
+
// of the new one, or of the caller a sign-out rotated it to (26.9), here rather than off its own
|
|
74
|
+
// request.
|
|
75
|
+
onSignInChanged((caller, now, signedIn) => {
|
|
76
|
+
for (const follow of FOLLOWERS.get(caller) ?? []) follow(now, signedIn)
|
|
77
|
+
})
|
|
78
|
+
|
|
68
79
|
/**
|
|
69
80
|
* What a pairing is filed under: the browser, and who was signed in there (`signedInAs`).
|
|
70
81
|
*
|
|
@@ -75,7 +86,7 @@ function pairingKey(caller: string, signedIn: string): string {
|
|
|
75
86
|
return `${caller}\n${signedIn}`
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
// 44.19 — the relay answers once the app has handed out a pairing
|
|
89
|
+
// 44.19 — the relay answers once the app has handed out a pairing code, and never before.
|
|
79
90
|
let minted = false
|
|
80
91
|
|
|
81
92
|
/** A single-use pairing code for `caller` signed in as `signedIn`, and when it stops standing. 44.14. */
|
|
@@ -93,24 +104,135 @@ export function mintCode(
|
|
|
93
104
|
return { code, expires }
|
|
94
105
|
}
|
|
95
106
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
/**
|
|
108
|
+
* One turn on the bridge paired to `caller` signed in as `signedIn`: sent `prompt` under `config`,
|
|
109
|
+
* yielding the events its Claude Code reports, each MCP request it makes handed to `answer` and the
|
|
110
|
+
* reply sent back. 44.13, 44.17, 44.21.
|
|
111
|
+
*/
|
|
112
|
+
export async function* runTurn({
|
|
113
|
+
caller,
|
|
114
|
+
signedIn,
|
|
115
|
+
prompt,
|
|
116
|
+
config,
|
|
117
|
+
maxTurns,
|
|
118
|
+
answer,
|
|
119
|
+
signal,
|
|
120
|
+
}: {
|
|
121
|
+
caller: string
|
|
122
|
+
signedIn: string
|
|
123
|
+
prompt: unknown[]
|
|
124
|
+
config: unknown
|
|
125
|
+
maxTurns: number
|
|
126
|
+
/** Never rejects: its failure is a reply too. */
|
|
127
|
+
answer: (method: string, params: unknown) => Promise<unknown>
|
|
128
|
+
signal: AbortSignal
|
|
129
|
+
}): AsyncGenerator<AssistantEvent> {
|
|
130
|
+
const bridge = BY_KEY.get(pairingKey(caller, signedIn))
|
|
131
|
+
// 44.13, 44.33 — nothing paired by this sign-in is the page's cue to show the command.
|
|
132
|
+
if (bridge === undefined)
|
|
133
|
+
throw refuse(
|
|
134
|
+
409,
|
|
135
|
+
'No Claude Code is paired with this browser as the one signed in. claudeCode.bridgeCommand() is the command that pairs one.',
|
|
136
|
+
)
|
|
137
|
+
const turn = randomToken(9, 'base64url')
|
|
138
|
+
const inbox = new Inbox<TurnItem>()
|
|
139
|
+
bridge.turns.set(turn, inbox)
|
|
140
|
+
// `bridge.frames` is read at each push: a resumed stream (44.26) replaces it.
|
|
141
|
+
bridge.frames.push({ type: 'turn', id: turn, prompt, config, maxTurns })
|
|
142
|
+
// 44.18 — a caller hanging up ends the run on the bridge.
|
|
143
|
+
const cancel = (): void => {
|
|
144
|
+
bridge.frames.push({ type: 'cancel', turn })
|
|
145
|
+
}
|
|
146
|
+
signal.addEventListener('abort', cancel)
|
|
147
|
+
try {
|
|
148
|
+
for await (const item of inbox) {
|
|
149
|
+
if (item.kind === 'event') yield item.event as AssistantEvent
|
|
150
|
+
else if (item.kind === 'mcp')
|
|
151
|
+
void answer(item.method, item.params).then((reply) => {
|
|
152
|
+
bridge.frames.push({
|
|
153
|
+
type: 'reply',
|
|
154
|
+
turn,
|
|
155
|
+
id: item.id,
|
|
156
|
+
reply,
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
else if (item.kind === 'end') return
|
|
160
|
+
else throw refuse(502, `Claude Code failed: ${item.message}`)
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
signal.removeEventListener('abort', cancel)
|
|
164
|
+
bridge.turns.delete(turn)
|
|
165
|
+
if (!WATCHERS.has(bridge.key)) armIdle(bridge.key)
|
|
166
|
+
}
|
|
102
167
|
}
|
|
103
168
|
|
|
104
169
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
170
|
+
* The pairing of `caller` signed in as `signedIn` as it changes, until `signal` aborts: the code that
|
|
171
|
+
* pairs it while unpaired, then `undefined` the moment a bridge connects, and a code again if it goes. A
|
|
172
|
+
* code that expires while nothing is paired is replaced with a fresh one. 44.20.
|
|
173
|
+
*
|
|
174
|
+
* 44.34 — IT FOLLOWS WHOEVER SIGNS IN AT THE BROWSER: a sign-in or a sign-out made since is the
|
|
175
|
+
* pairing it watches from then on, and the code it offers is that sign-in's.
|
|
107
176
|
*/
|
|
108
|
-
export function
|
|
177
|
+
export async function* pairingStatus(
|
|
109
178
|
caller: string,
|
|
110
179
|
signedIn: string,
|
|
111
|
-
|
|
112
|
-
):
|
|
113
|
-
|
|
180
|
+
signal: AbortSignal,
|
|
181
|
+
): AsyncGenerator<string | undefined> {
|
|
182
|
+
// A change landing while the loop was not waiting — paused at its `yield` — stays queued, so it
|
|
183
|
+
// looks again; a look that finds nothing new yields nothing.
|
|
184
|
+
const changes = new Inbox<undefined>()
|
|
185
|
+
const waiting = changes[Symbol.asyncIterator]()
|
|
186
|
+
const rouse = (): void => changes.push(undefined)
|
|
187
|
+
const follow = (now: string, next: string): void => {
|
|
188
|
+
caller = now
|
|
189
|
+
signedIn = next
|
|
190
|
+
rouse()
|
|
191
|
+
}
|
|
192
|
+
let watchingKey = ''
|
|
193
|
+
let unwatch = (): void => {}
|
|
194
|
+
let unfollow = (): void => {}
|
|
195
|
+
let pairing: { code: string; expires: number } | undefined
|
|
196
|
+
// `null` until the first is said; `undefined` says paired.
|
|
197
|
+
let said: string | undefined | null = null
|
|
198
|
+
signal.addEventListener('abort', rouse)
|
|
199
|
+
try {
|
|
200
|
+
while (!signal.aborted) {
|
|
201
|
+
const key = pairingKey(caller, signedIn)
|
|
202
|
+
if (key !== watchingKey) {
|
|
203
|
+
unwatch()
|
|
204
|
+
unfollow()
|
|
205
|
+
unwatch = watchPairing(key, rouse)
|
|
206
|
+
unfollow = followSignIn(caller, follow)
|
|
207
|
+
watchingKey = key
|
|
208
|
+
pairing = undefined
|
|
209
|
+
}
|
|
210
|
+
const paired = BY_KEY.has(key)
|
|
211
|
+
if (
|
|
212
|
+
!paired &&
|
|
213
|
+
(pairing === undefined || pairing.expires <= Date.now())
|
|
214
|
+
)
|
|
215
|
+
pairing = mintCode(caller, signedIn)
|
|
216
|
+
const code = paired ? undefined : pairing?.code
|
|
217
|
+
if (code !== said) {
|
|
218
|
+
said = code
|
|
219
|
+
yield code
|
|
220
|
+
}
|
|
221
|
+
const renew = paired
|
|
222
|
+
? undefined
|
|
223
|
+
: setTimeout(rouse, (pairing?.expires ?? 0) - Date.now())
|
|
224
|
+
await waiting.next()
|
|
225
|
+
clearTimeout(renew)
|
|
226
|
+
}
|
|
227
|
+
} finally {
|
|
228
|
+
signal.removeEventListener('abort', rouse)
|
|
229
|
+
unwatch()
|
|
230
|
+
unfollow()
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Registers `wake` to run whenever the pairing filed under `key` changes; the returned function undoes it. */
|
|
235
|
+
function watchPairing(key: string, wake: () => void): () => void {
|
|
114
236
|
let watchers = WATCHERS.get(key)
|
|
115
237
|
if (watchers === undefined) {
|
|
116
238
|
watchers = new Set()
|
|
@@ -131,22 +253,20 @@ export function watchPairing(
|
|
|
131
253
|
}
|
|
132
254
|
}
|
|
133
255
|
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (!WATCHERS.has(bridge.key)) armIdle(bridge.key)
|
|
149
|
-
},
|
|
256
|
+
/** Registers `follow` to hear who signs in at `caller` from here on; the returned function undoes it. */
|
|
257
|
+
function followSignIn(
|
|
258
|
+
caller: string,
|
|
259
|
+
follow: (caller: string, signedIn: string) => void,
|
|
260
|
+
): () => void {
|
|
261
|
+
let followers = FOLLOWERS.get(caller)
|
|
262
|
+
if (followers === undefined) {
|
|
263
|
+
followers = new Set()
|
|
264
|
+
FOLLOWERS.set(caller, followers)
|
|
265
|
+
}
|
|
266
|
+
followers.add(follow)
|
|
267
|
+
return () => {
|
|
268
|
+
followers.delete(follow)
|
|
269
|
+
if (followers.size === 0) FOLLOWERS.delete(caller)
|
|
150
270
|
}
|
|
151
271
|
}
|
|
152
272
|
|
|
@@ -171,38 +291,6 @@ function wakeWatchers(key: string): void {
|
|
|
171
291
|
for (const wake of WATCHERS.get(key) ?? []) wake()
|
|
172
292
|
}
|
|
173
293
|
|
|
174
|
-
/**
|
|
175
|
-
* Registers `follow` to hear who signs in at `caller` from here on, and the caller a sign-out rotates
|
|
176
|
-
* it to (26.9); the returned function undoes it.
|
|
177
|
-
*
|
|
178
|
-
* 44.34 — A STATUS STREAM OPENED BEFORE A SIGN-IN carries the cookies it was opened with, so it hears
|
|
179
|
-
* of the new one here rather than off its own request.
|
|
180
|
-
*/
|
|
181
|
-
export function followSignIn(
|
|
182
|
-
caller: string,
|
|
183
|
-
follow: (caller: string, signedIn: string) => void,
|
|
184
|
-
): () => void {
|
|
185
|
-
let followers = FOLLOWERS.get(caller)
|
|
186
|
-
if (followers === undefined) {
|
|
187
|
-
followers = new Set()
|
|
188
|
-
FOLLOWERS.set(caller, followers)
|
|
189
|
-
}
|
|
190
|
-
followers.add(follow)
|
|
191
|
-
return () => {
|
|
192
|
-
followers.delete(follow)
|
|
193
|
-
if (followers.size === 0) FOLLOWERS.delete(caller)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Tells every status stream at `caller` that the browser is `now`, signed in as `signedIn`. 44.34. */
|
|
198
|
-
export function signedInAt(
|
|
199
|
-
caller: string,
|
|
200
|
-
now: string,
|
|
201
|
-
signedIn: string,
|
|
202
|
-
): void {
|
|
203
|
-
for (const follow of FOLLOWERS.get(caller) ?? []) follow(now, signedIn)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
294
|
function item(
|
|
207
295
|
kind: TurnItem['kind'],
|
|
208
296
|
fields: {
|