@agentshouse/kit 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +722 -263
- package/bin/connect-linux.sh +200 -25
- package/bin/kit.js +179 -157
- package/bootstrap.json +17 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +76 -12
- package/src/agents.js +63 -8
- package/src/binding.js +52 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +28 -14
- package/src/bytes.js +19 -8
- package/src/configuration.js +13 -209
- package/src/connector-login.js +3 -84
- package/src/contract.js +36 -11
- package/src/delivery.js +4 -7
- package/src/directory.js +6 -26
- package/src/draft.js +4 -82
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- package/src/google/calendar.js +39 -45
- package/src/google/drive.js +61 -186
- package/src/google/entries.js +346 -0
- package/src/google/gateway.js +3 -6
- package/src/google/gmail.js +33 -23
- package/src/google/oauth.js +106 -66
- package/src/google/source.js +21 -23
- package/src/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +446 -127
- package/src/how-we-work.md +25 -0
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +118 -0
- package/src/launcher.js +46 -10
- package/src/local-files.js +3 -11
- package/src/login.js +23 -11
- package/src/mcp.js +18 -0
- package/src/mount.js +13 -21
- package/src/original.js +4 -4
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +14 -15
- package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/src/skill-set/ask-matt/SKILL.md +91 -0
- package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
- package/src/skill-set/code-review/LICENSE +21 -0
- package/src/skill-set/code-review/SKILL.md +89 -0
- package/src/skill-set/code-review/agents/openai.yaml +3 -0
- package/src/skill-set/codebase-design/DEEPENING.md +37 -0
- package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/src/skill-set/codebase-design/LICENSE +21 -0
- package/src/skill-set/codebase-design/SKILL.md +116 -0
- package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
- package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
- package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
- package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/src/skill-set/domain-modeling/LICENSE +21 -0
- package/src/skill-set/domain-modeling/SKILL.md +76 -0
- package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
- package/src/skill-set/grill-me/LICENSE +21 -0
- package/src/skill-set/grill-me/SKILL.md +8 -0
- package/src/skill-set/grill-me/agents/openai.yaml +4 -0
- package/src/skill-set/grill-with-docs/LICENSE +21 -0
- package/src/skill-set/grill-with-docs/SKILL.md +8 -0
- package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
- package/src/skill-set/grilling/LICENSE +21 -0
- package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
- package/src/skill-set/handoff/LICENSE +21 -0
- package/src/skill-set/handoff/SKILL.md +17 -0
- package/src/skill-set/handoff/agents/openai.yaml +4 -0
- package/src/skill-set/implement/LICENSE +21 -0
- package/src/skill-set/implement/SKILL.md +16 -0
- package/src/skill-set/implement/agents/openai.yaml +4 -0
- package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
- package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
- package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
- package/src/skill-set/prototype/LICENSE +21 -0
- package/src/skill-set/prototype/LOGIC.md +67 -0
- package/src/skill-set/prototype/SKILL.md +28 -0
- package/src/skill-set/prototype/UI.md +112 -0
- package/src/skill-set/prototype/agents/openai.yaml +3 -0
- package/src/skill-set/research/LICENSE +21 -0
- package/src/skill-set/research/SKILL.md +14 -0
- package/src/skill-set/research/agents/openai.yaml +3 -0
- package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
- package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
- package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/LICENSE +21 -0
- package/src/skill-set/tdd/SKILL.md +40 -0
- package/src/skill-set/tdd/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/mocking.md +59 -0
- package/src/skill-set/tdd/tests.md +77 -0
- package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
- package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/src/skill-set/teach/LICENSE +21 -0
- package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
- package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
- package/src/skill-set/teach/SKILL.md +141 -0
- package/src/skill-set/teach/agents/openai.yaml +4 -0
- package/src/skill-set/to-questionnaire/LICENSE +21 -0
- package/src/skill-set/to-questionnaire/SKILL.md +55 -0
- package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
- package/src/skill-set/to-spec/LICENSE +21 -0
- package/src/skill-set/to-spec/SKILL.md +76 -0
- package/src/skill-set/to-spec/agents/openai.yaml +4 -0
- package/src/skill-set/to-tickets/LICENSE +21 -0
- package/src/skill-set/to-tickets/SKILL.md +106 -0
- package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
- package/src/skill-set/wait-what/LICENSE +21 -0
- package/src/skill-set/wait-what/SKILL.md +8 -0
- package/src/skill-set/wait-what/agents/openai.yaml +4 -0
- package/src/skill-set/wayfinder/LICENSE +21 -0
- package/src/skill-set/wayfinder/SKILL.md +129 -0
- package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
- package/src/skill-set/writing-for-agents/LICENSE +21 -0
- package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/src/skill-set/writing-for-agents/SKILL.md +83 -0
- package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
- package/src/skill-set.js +85 -0
- package/src/source.js +93 -54
- package/src/state.js +19 -1
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +16 -3
- package/src/sync.js +129 -41
- package/src/tasks.js +75 -0
- package/src/telegram-view.js +214 -0
- package/src/telegram.js +161 -66
- package/src/turn-frames.js +69 -0
- package/src/worker.js +685 -184
- package/src/working-copy.js +1221 -0
- package/src/workspace.js +40 -32
- package/src/google/declared.js +0 -146
- package/src/google/id.js +0 -1
- package/src/instance/google.js +0 -1
- package/src/starter-set.js +0 -17
- /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
- /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
package/src/worker.js
CHANGED
|
@@ -1,66 +1,91 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
2
4
|
import { join } from "node:path";
|
|
3
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
installedAgentCommand,
|
|
9
|
+
readAgentState,
|
|
10
|
+
reconcileAgentKinds,
|
|
11
|
+
} from "./agents.js";
|
|
6
12
|
import { readBindings, removeBinding, writeBinding } from "./binding.js";
|
|
7
13
|
import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
|
|
8
|
-
import { openBridge } from "./bridge.js";
|
|
14
|
+
import { bridgedFrame, openBridge } from "./bridge.js";
|
|
15
|
+
import { transferBytes } from "./bytes.js";
|
|
9
16
|
import { assistantBlocks } from "./blocks.js";
|
|
10
17
|
import { retryConversationFiles } from "./conversation-originals.js";
|
|
11
|
-
import {
|
|
12
|
-
import { localReply, localRequest, reconcileInbound } from "./inbound.js";
|
|
18
|
+
import { localRequest, reconcileInbound } from "./inbound.js";
|
|
13
19
|
import { localFiles } from "./local-files.js";
|
|
20
|
+
import { answerText, answerYaml, editRefusal } from "./mcp.js";
|
|
14
21
|
import { inheritedEnvironment } from "./command.js";
|
|
22
|
+
import { sourceEntries } from "./google/entries.js";
|
|
23
|
+
import { watchKitWork } from "./kit-work-stream.js";
|
|
24
|
+
import { finalPlan } from "./plan.js";
|
|
25
|
+
import { coalescedTask, residentWake } from "./tasks.js";
|
|
26
|
+
import {
|
|
27
|
+
applyWorkingCopies,
|
|
28
|
+
deliverWorkingCopies,
|
|
29
|
+
ownerCaller,
|
|
30
|
+
pushWorkingCopy,
|
|
31
|
+
recordIndexChange,
|
|
32
|
+
uploadedPaths,
|
|
33
|
+
workingCopyPaths,
|
|
34
|
+
} from "./working-copy.js";
|
|
15
35
|
import { readConfiguration } from "./configuration.js";
|
|
16
36
|
import { launchBase, readyLaunchDirectory } from "./directory.js";
|
|
17
37
|
import {
|
|
18
38
|
BRIDGE_NONCE_ENVIRONMENT,
|
|
19
39
|
BRIDGE_SOCKET_ENVIRONMENT,
|
|
40
|
+
MCP_REQUEST_BODY_MAX_BYTES,
|
|
20
41
|
PROVIDER_CONTINUATION_UNAVAILABLE,
|
|
21
42
|
WORKER_COMMAND_BATCH_MAX,
|
|
22
|
-
WORKER_COMMAND_POLL_MS,
|
|
23
43
|
WORKER_EXIT_GRACE_MS,
|
|
24
44
|
WORKER_EXIT_POLL_MS,
|
|
25
|
-
WORKER_HEARTBEAT_MS,
|
|
26
45
|
WORKER_INHERITED_ENVIRONMENT,
|
|
27
46
|
WORKER_SUPERVISION_POLL_MS,
|
|
47
|
+
WORKING_COPY_INDEX_METHOD,
|
|
48
|
+
WORKING_COPY_PUSH_METHOD,
|
|
49
|
+
WORKING_COPY_UPLOAD_COMMAND,
|
|
28
50
|
} from "./contract.js";
|
|
29
|
-
import { EpochReportRequired, houseFailed, KitRefusal } from "./refusal.js";
|
|
51
|
+
import { EpochReportRequired, formatCommandError, houseFailed, KitRefusal } from "./refusal.js";
|
|
30
52
|
import {
|
|
31
53
|
acceptAssistantText,
|
|
32
54
|
acquireCommands,
|
|
33
55
|
acquireSetup,
|
|
34
|
-
reportAgentCapabilities,
|
|
35
56
|
acquireOneRun,
|
|
36
57
|
acquireOneResumption,
|
|
37
58
|
admitInteraction,
|
|
38
|
-
admitPlanSnapshot,
|
|
39
59
|
awaitSecretInput,
|
|
40
60
|
rejectResumption,
|
|
41
61
|
callHouseMcp,
|
|
42
62
|
commitRun,
|
|
43
|
-
|
|
63
|
+
controlSleep,
|
|
44
64
|
maintainControl,
|
|
45
65
|
mutatingCall,
|
|
46
66
|
observeSessionStart,
|
|
47
67
|
observeSessionStop,
|
|
48
68
|
openSession,
|
|
69
|
+
readRunRequest,
|
|
49
70
|
replaceSessionAuthority,
|
|
50
71
|
reportContinuation,
|
|
51
72
|
reportInterruptOutcome,
|
|
52
73
|
reportInteractionOutcome,
|
|
53
74
|
toolMutations,
|
|
54
75
|
reportRunOutcome,
|
|
55
|
-
|
|
76
|
+
releaseControl,
|
|
56
77
|
reportSessions,
|
|
78
|
+
serveResident,
|
|
57
79
|
sessionAuthority,
|
|
58
|
-
|
|
80
|
+
updateDrain,
|
|
59
81
|
} from "./house.js";
|
|
60
82
|
import { operationId } from "./operation.js";
|
|
61
|
-
import { runSetup } from "./setup.js";
|
|
83
|
+
import { runSetup, signInWantedAgent } from "./setup.js";
|
|
62
84
|
import { readSettlements, removeSettlement, writeSettlement } from "./settlement.js";
|
|
85
|
+
import { howWeWorkOwed, noteHowWeWork, reconcileSkillSet } from "./skill-set.js";
|
|
86
|
+
import { writeText } from "./state.js";
|
|
63
87
|
import { failedReport, terminalReport } from "./terminal.js";
|
|
88
|
+
import { kitStreamWriter, turnFrames } from "./turn-frames.js";
|
|
64
89
|
import {
|
|
65
90
|
createSession,
|
|
66
91
|
destroySession,
|
|
@@ -71,47 +96,32 @@ import {
|
|
|
71
96
|
} from "./tmux.js";
|
|
72
97
|
|
|
73
98
|
const LAUNCHER_MAIN = fileURLToPath(new URL("launcher-main.js", import.meta.url));
|
|
99
|
+
const RESIDENT_INTERVAL_MS = 30_000;
|
|
100
|
+
const PASS_REACHES = ["commands", "runs"];
|
|
74
101
|
|
|
75
102
|
async function dispatchSeal(context, bound) {
|
|
76
103
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return { accepted, failure: null, unresolved: false, refused: null };
|
|
104
|
+
await acceptAssistantText(context.session, bound.session, bound.sealed, bound.plan);
|
|
105
|
+
bound.stopped.textAccepted = true;
|
|
80
106
|
} catch (refusal) {
|
|
81
107
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
82
108
|
const named = { code: refusal.code, detail: refusal.message };
|
|
83
|
-
if (undecided(refusal)) return
|
|
84
|
-
|
|
85
|
-
return { accepted: null, failure: named, unresolved: false, refused: null };
|
|
109
|
+
if (undecided(context, refusal)) return named;
|
|
110
|
+
bound.stopped.failure ??= named;
|
|
86
111
|
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function resolveSeal(context, bound) {
|
|
90
112
|
bound.sealed = null;
|
|
91
113
|
await writeBinding(context.directory, bound);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
async function sealAssistantText(context, bound, said) {
|
|
95
|
-
const blocks = assistantBlocks(said.join("\n\n"));
|
|
96
|
-
if (blocks === null) return { accepted: null, failure: null, unresolved: false, refused: null };
|
|
97
|
-
bound.sealed = { operation: operationId(), blocks };
|
|
98
|
-
await writeBinding(context.directory, bound);
|
|
99
|
-
return dispatchSeal(context, bound);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function recoverAssistantSeal(context, binding) {
|
|
103
|
-
if (binding.sealed === null) return { unresolved: false, refused: null };
|
|
104
|
-
const sealed = await dispatchSeal(context, binding);
|
|
105
|
-
return { unresolved: sealed.unresolved, refused: sealed.refused };
|
|
114
|
+
return null;
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
function unresolvedRun(bound, refused = null) {
|
|
109
118
|
return { run: bound.run, session: bound.session, refused };
|
|
110
119
|
}
|
|
111
120
|
|
|
112
|
-
function undecided(refusal) {
|
|
121
|
+
function undecided(context, refusal) {
|
|
113
122
|
return (
|
|
114
123
|
houseFailed(refusal) ||
|
|
124
|
+
context.session.refused === true ||
|
|
115
125
|
(refusal instanceof KitRefusal &&
|
|
116
126
|
["kit_command_cancelled", "kit_session_token_rejected"].includes(refusal.code))
|
|
117
127
|
);
|
|
@@ -121,7 +131,7 @@ async function pendingCommands(context, kinds = ["stop"]) {
|
|
|
121
131
|
try {
|
|
122
132
|
return await acquireCommands(context.session, WORKER_COMMAND_BATCH_MAX, kinds);
|
|
123
133
|
} catch (refusal) {
|
|
124
|
-
if (houseFailed(refusal)
|
|
134
|
+
if (houseFailed(refusal) || context.session.refused === true) return null;
|
|
125
135
|
throw refusal;
|
|
126
136
|
}
|
|
127
137
|
}
|
|
@@ -142,14 +152,6 @@ async function settleInterrupt(context, interrupt, outcome) {
|
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
|
|
145
|
-
async function reportHeartbeat(context, bound) {
|
|
146
|
-
try {
|
|
147
|
-
await reportSessionHeartbeat(context.session, bound.session);
|
|
148
|
-
} catch (refusal) {
|
|
149
|
-
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
155
|
function retryFiles(context) {
|
|
154
156
|
if (context.retrying !== undefined) return;
|
|
155
157
|
context.retrying = retryConversationFiles(context)
|
|
@@ -172,39 +174,32 @@ async function settleFiles(context) {
|
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
async function supervise(context, bound, bridge) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await
|
|
184
|
-
if (
|
|
185
|
-
if (Date.now() >= commanded) {
|
|
186
|
-
commanded = Date.now() + WORKER_COMMAND_POLL_MS;
|
|
177
|
+
const departed = new AbortController();
|
|
178
|
+
bridge.departed.then(() => departed.abort());
|
|
179
|
+
const waiting = AbortSignal.any([context.signal, departed.signal]);
|
|
180
|
+
let owed = false;
|
|
181
|
+
for (;;) {
|
|
182
|
+
const commanded = (await context.commands.wait(WORKER_SUPERVISION_POLL_MS, waiting)) || owed;
|
|
183
|
+
if (departed.signal.aborted) return null;
|
|
184
|
+
if (context.signal.aborted) throw new KitRefusal("kit_command_cancelled");
|
|
185
|
+
await context.update?.announce();
|
|
186
|
+
if (commanded) {
|
|
187
187
|
const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
|
|
188
|
-
|
|
188
|
+
owed = polled === null && context.session.refused !== true;
|
|
189
|
+
for (const answer of polled?.answers ?? []) {
|
|
189
190
|
if (answer.session === bound.session) bridge.answered(answer);
|
|
190
191
|
}
|
|
191
|
-
for (const interrupt of polled
|
|
192
|
+
for (const interrupt of polled?.interrupts ?? []) {
|
|
192
193
|
if (interrupt.session !== bound.session || interrupt.run !== bound.run) continue;
|
|
193
194
|
if (!bridge.interrupt(interrupt)) {
|
|
194
195
|
await settleInterrupt(context, interrupt.interrupt, "nothing_to_interrupt");
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
|
-
const stop = polled
|
|
198
|
+
const stop = polled?.stops.find((command) => command.run === bound.run);
|
|
198
199
|
if (stop !== undefined) return stop;
|
|
199
200
|
}
|
|
200
|
-
if (Date.now() >= pulsed) {
|
|
201
|
-
pulsed = Date.now() + WORKER_HEARTBEAT_MS;
|
|
202
|
-
retryFiles(context);
|
|
203
|
-
await reportHeartbeat(context, bound);
|
|
204
|
-
}
|
|
205
201
|
if (!(await sessionPresent(context.runtime, bound.tmux))) return null;
|
|
206
202
|
}
|
|
207
|
-
return null;
|
|
208
203
|
}
|
|
209
204
|
|
|
210
205
|
async function grantExitGrace(runtime, handle) {
|
|
@@ -221,9 +216,9 @@ async function completeOwnerStop(context, bound, requestedAt) {
|
|
|
221
216
|
const destroyed = await destroySession(context.runtime, bound.tmux);
|
|
222
217
|
let failure = null;
|
|
223
218
|
try {
|
|
224
|
-
await observeSessionStop(context.session, bound.session);
|
|
219
|
+
await observeSessionStop(context.session, bound.session, false, bound.plan);
|
|
225
220
|
} catch (refusal) {
|
|
226
|
-
if (
|
|
221
|
+
if (undecided(context, refusal)) return { destroyed, confirmed: false, failure: null };
|
|
227
222
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
228
223
|
failure = { code: refusal.code, detail: refusal.message };
|
|
229
224
|
}
|
|
@@ -234,10 +229,10 @@ async function completeOwnerStop(context, bound, requestedAt) {
|
|
|
234
229
|
|
|
235
230
|
async function settleReported(context, bound) {
|
|
236
231
|
try {
|
|
237
|
-
await reportRunOutcome(context.session, bound.run, bound.reported);
|
|
232
|
+
await reportRunOutcome(context.session, bound.run, bound.reported, bound.plan);
|
|
238
233
|
return { settled: true, failure: null };
|
|
239
234
|
} catch (refusal) {
|
|
240
|
-
if (undecided(refusal)) return { settled: false, failure: null };
|
|
235
|
+
if (undecided(context, refusal)) return { settled: false, failure: null };
|
|
241
236
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
242
237
|
if (refusal.code === "worker_run_settled") return { settled: true, failure: null };
|
|
243
238
|
return { settled: false, failure: { code: refusal.code, detail: refusal.message } };
|
|
@@ -272,41 +267,134 @@ function endOnEpochReport(ended, act) {
|
|
|
272
267
|
};
|
|
273
268
|
}
|
|
274
269
|
|
|
270
|
+
function refusedWith(text) {
|
|
271
|
+
const { code, conflicts } = editRefusal(text);
|
|
272
|
+
const current =
|
|
273
|
+
conflicts?.map(({ path, state, revision }) =>
|
|
274
|
+
state === "absent" ? { path, revision: null, content: null } : { path, revision },
|
|
275
|
+
) ?? null;
|
|
276
|
+
return { refused: { code, text, current } };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function acceptedWith(text) {
|
|
280
|
+
const written = new Map();
|
|
281
|
+
for (const { path, op, revision, from } of answerYaml(text).results) {
|
|
282
|
+
if (from !== undefined) written.set(from, { path: from, revision: null, content: null });
|
|
283
|
+
written.set(path, op === "remove" ? { path, revision: null, content: null } : { path, revision });
|
|
284
|
+
}
|
|
285
|
+
return { refused: null, written: [...written.values()] };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function toolFrame(name, args, operation) {
|
|
289
|
+
return bridgedFrame({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }, operation);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function uploadBody(changes) {
|
|
293
|
+
const body = Buffer.from(JSON.stringify({ changes }), "utf8");
|
|
294
|
+
if (body.length > MCP_REQUEST_BODY_MAX_BYTES) return body;
|
|
295
|
+
return Buffer.concat([body, Buffer.alloc(MCP_REQUEST_BODY_MAX_BYTES + 1 - body.length, " ")]);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function sessionPush(context, bound, authority, frame, signal) {
|
|
299
|
+
const call = async (sent, called) =>
|
|
300
|
+
callHouseMcp(context.session, await sessionAuthority(context.session, bound.session, authority, sent), called, sent);
|
|
301
|
+
const refused = (answer) =>
|
|
302
|
+
answer.error !== undefined
|
|
303
|
+
? refusedWith(answer.error.message ?? "kit_house_refused")
|
|
304
|
+
: answer.result?.isError === true
|
|
305
|
+
? refusedWith(answerText(answer.result))
|
|
306
|
+
: null;
|
|
307
|
+
const stage = async (body, sent) => {
|
|
308
|
+
const bytes = uploadBody(body.changes);
|
|
309
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
310
|
+
const prepare = { paths: uploadedPaths(body.changes), bytes: bytes.length, sha256 };
|
|
311
|
+
const prepared = await call(
|
|
312
|
+
sent,
|
|
313
|
+
toolFrame("run_command", { command: WORKING_COPY_UPLOAD_COMMAND, arguments: prepare }, body.operation_id),
|
|
314
|
+
);
|
|
315
|
+
const refusal = refused(prepared);
|
|
316
|
+
if (refusal !== null) return refusal;
|
|
317
|
+
const transfer = answerYaml(answerText(prepared.result))?.transfer;
|
|
318
|
+
const staged = (
|
|
319
|
+
await transferBytes(
|
|
320
|
+
{ url: transfer?.url, method: "POST", operation: transfer?.operation, body: bytes, framed: true, signal: sent },
|
|
321
|
+
context.session.dependencies,
|
|
322
|
+
)
|
|
323
|
+
).transfer;
|
|
324
|
+
if (typeof staged?.url !== "string" || typeof staged.operation !== "string") {
|
|
325
|
+
throw new KitRefusal("kit_house_answer_unreadable", WORKING_COPY_UPLOAD_COMMAND);
|
|
326
|
+
}
|
|
327
|
+
return { upload: { url: staged.url, operation: staged.operation, bytes: bytes.length, sha256 } };
|
|
328
|
+
};
|
|
329
|
+
const edit = async (body, sent, retain) => {
|
|
330
|
+
let { upload } = body;
|
|
331
|
+
const inline = toolFrame("edit", { changes: body.changes }, body.operation_id);
|
|
332
|
+
if (upload === undefined && Buffer.byteLength(JSON.stringify(inline)) > MCP_REQUEST_BODY_MAX_BYTES) {
|
|
333
|
+
const staged = await stage(body, sent);
|
|
334
|
+
if (staged.upload === undefined) return staged;
|
|
335
|
+
({ upload } = staged);
|
|
336
|
+
await retain(upload);
|
|
337
|
+
}
|
|
338
|
+
const answer = await call(
|
|
339
|
+
sent,
|
|
340
|
+
upload === undefined ? inline : toolFrame("edit", { paths: uploadedPaths(body.changes), upload }, body.operation_id),
|
|
341
|
+
);
|
|
342
|
+
return refused(answer) ?? acceptedWith(answerText(answer.result));
|
|
343
|
+
};
|
|
344
|
+
let outcome;
|
|
345
|
+
try {
|
|
346
|
+
outcome = await pushWorkingCopy(context, frame.params, { binding: { session: bound.session }, submit: edit }, signal);
|
|
347
|
+
} catch (refusal) {
|
|
348
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
349
|
+
outcome = { refused: true, text: refusal.message };
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
jsonrpc: "2.0",
|
|
353
|
+
id: frame.id,
|
|
354
|
+
result: { content: [{ type: "text", text: outcome.text }], ...(outcome.refused ? { isError: true } : {}) },
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
275
358
|
async function openRunBridge(context, bound, authority, start) {
|
|
276
359
|
const ended = new AbortController();
|
|
277
360
|
const handed = (act) => endOnEpochReport(ended, act);
|
|
278
|
-
const
|
|
279
|
-
send: handed((blocks) => streamAssistantDraft(context.session, bound.session, blocks)),
|
|
280
|
-
});
|
|
361
|
+
const frames = turnFrames(context.stream);
|
|
281
362
|
const bridge = await openBridge({
|
|
282
363
|
socket: bound.socket,
|
|
283
364
|
nonce: bound.nonce,
|
|
284
365
|
signal: AbortSignal.any([context.signal, ended.signal]),
|
|
285
366
|
mutates: mutatingCall(await toolMutations(context.session, context.signal)),
|
|
286
367
|
alive: () => sessionPresent(context.runtime, bound.tmux),
|
|
287
|
-
draft: (
|
|
368
|
+
draft: (view) => frames.draft(view),
|
|
369
|
+
plan: (snapshot) => frames.plan(snapshot),
|
|
288
370
|
start,
|
|
289
371
|
question: handed((batch) => admitInteraction(context.session, bound.session, batch)),
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
372
|
+
howWeWork: () =>
|
|
373
|
+
noteHowWeWork(context.directory).catch((error) =>
|
|
374
|
+
reported(`How-we-work note unwritable (${error.code ?? "unknown"})`),
|
|
375
|
+
),
|
|
376
|
+
interrupt: handed((interrupt, outcome, plan) =>
|
|
377
|
+
reportInterruptOutcome(context.session, interrupt, outcome, finalPlan(plan)),
|
|
293
378
|
),
|
|
294
379
|
secret: (interaction, ceremony) => awaitSecretInput(context.session, interaction, ceremony),
|
|
295
380
|
settle: handed((interaction, outcome) => settleInteraction(context, interaction, outcome)),
|
|
296
|
-
forward: handed((frame, signal) =>
|
|
297
|
-
|
|
298
|
-
|
|
381
|
+
forward: handed((frame, signal) => frame.method === WORKING_COPY_PUSH_METHOD
|
|
382
|
+
? sessionPush(context, bound, authority, frame, signal)
|
|
383
|
+
: frame.method === WORKING_COPY_INDEX_METHOD
|
|
384
|
+
? recordIndexChange(context, frame.params, signal).then(() => ({ jsonrpc: "2.0", id: frame.id, result: {} }))
|
|
385
|
+
: localFiles(async (sent) =>
|
|
386
|
+
callHouseMcp(
|
|
299
387
|
context.session,
|
|
300
388
|
await sessionAuthority(context.session, bound.session, authority, signal),
|
|
301
389
|
sent,
|
|
302
390
|
signal,
|
|
303
|
-
)
|
|
391
|
+
), context.session.dependencies, signal)(frame)),
|
|
304
392
|
});
|
|
305
|
-
return { bridge,
|
|
393
|
+
return { bridge, frames, ended };
|
|
306
394
|
}
|
|
307
395
|
|
|
308
396
|
async function superviseBound(context, bound, opened, start) {
|
|
309
|
-
const { bridge,
|
|
397
|
+
const { bridge, ended } = opened;
|
|
310
398
|
const spawn = { committed: false };
|
|
311
399
|
let unsupervised = null;
|
|
312
400
|
let destroyed = false;
|
|
@@ -320,23 +408,20 @@ async function superviseBound(context, bound, opened, start) {
|
|
|
320
408
|
unsupervised = { code: refusal.code, detail: refusal.message };
|
|
321
409
|
} finally {
|
|
322
410
|
try {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
} finally {
|
|
329
|
-
bridge.stopRecovery();
|
|
330
|
-
if (owner === null) {
|
|
331
|
-
try {
|
|
332
|
-
destroyed = await grantExitGrace(context.runtime, bound.tmux);
|
|
333
|
-
} finally {
|
|
334
|
-
await bridge.close();
|
|
335
|
-
}
|
|
336
|
-
}
|
|
411
|
+
retryFiles(context);
|
|
412
|
+
await settleFiles(context);
|
|
413
|
+
for (const interaction of bridge.unsettledInteractions()) {
|
|
414
|
+
await settleInteraction(context, interaction, "expired");
|
|
337
415
|
}
|
|
338
416
|
} finally {
|
|
339
|
-
|
|
417
|
+
bridge.stopRecovery();
|
|
418
|
+
if (owner === null) {
|
|
419
|
+
try {
|
|
420
|
+
destroyed = await grantExitGrace(context.runtime, bound.tmux);
|
|
421
|
+
} finally {
|
|
422
|
+
await bridge.close();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
340
425
|
}
|
|
341
426
|
}
|
|
342
427
|
ended.signal.throwIfAborted();
|
|
@@ -352,6 +437,7 @@ async function superviseBound(context, bound, opened, start) {
|
|
|
352
437
|
destroyed,
|
|
353
438
|
stopped: null,
|
|
354
439
|
};
|
|
440
|
+
bound.plan = finalPlan(bridge.plan());
|
|
355
441
|
if (owner !== null) {
|
|
356
442
|
const finished = await completeOwnerStop(context, bound, owner.requestedAt);
|
|
357
443
|
return {
|
|
@@ -371,50 +457,65 @@ async function superviseBound(context, bound, opened, start) {
|
|
|
371
457
|
held !== null &&
|
|
372
458
|
stopped?.providerContinuation === true &&
|
|
373
459
|
stopped.providerSessionId !== null;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
? await sealAssistantText(context, bound, outcome.assistant)
|
|
377
|
-
: { accepted: null, failure: null, unresolved: false, refused: null };
|
|
378
|
-
if (sealed.unresolved) return { ...outcome, refused: sealed.refused, settled: false };
|
|
379
|
-
if (sealed.failure !== null) outcome.failure ??= sealed.failure;
|
|
380
|
-
const execution = outcome.failure;
|
|
381
|
-
try {
|
|
382
|
-
await observeSessionStop(context.session, bound.session, sealed.accepted !== null);
|
|
383
|
-
} catch (refusal) {
|
|
384
|
-
if (!(refusal instanceof KitRefusal) || refusal.code !== "conversation_final_text_pending") throw refusal;
|
|
385
|
-
outcome.failure ??= { code: refusal.code, detail: refusal.message };
|
|
386
|
-
await observeSessionStop(context.session, bound.session);
|
|
460
|
+
if (continuable) {
|
|
461
|
+
await writeContinuation(context.directory, { ...held, providerSessionId: stopped.providerSessionId });
|
|
387
462
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
463
|
+
const turnCompleted = stopped?.stopOutcome === "completed";
|
|
464
|
+
const blocks = turnCompleted ? assistantBlocks(outcome.assistant.join("\n\n")) : null;
|
|
465
|
+
bound.sealed = blocks === null ? null : { operation: operationId(), blocks };
|
|
466
|
+
bound.stopped = {
|
|
467
|
+
turnCompleted,
|
|
391
468
|
capacityExhausted,
|
|
392
469
|
providerRefused,
|
|
393
|
-
failure: execution,
|
|
394
470
|
continuable,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
471
|
+
failure: outcome.failure,
|
|
472
|
+
textAccepted: false,
|
|
473
|
+
};
|
|
474
|
+
await writeBinding(context.directory, bound);
|
|
475
|
+
return { ...outcome, ...(await settleStopped(context, bound)) };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function stoppedReport(bound) {
|
|
479
|
+
if (bound.stopped === null || bound.stopped === undefined) return null;
|
|
480
|
+
return terminalReport({ ...bound.stopped, conversation: bound.conversation === true });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function settleStopped(context, bound) {
|
|
484
|
+
const { stopped } = bound;
|
|
485
|
+
const unresolved = (refused) => ({ failure: stopped.failure, refused, settled: false });
|
|
486
|
+
if (bound.sealed !== null) {
|
|
487
|
+
const refused = await dispatchSeal(context, bound);
|
|
488
|
+
if (refused !== null) return unresolved(refused);
|
|
489
|
+
}
|
|
490
|
+
let failure = stopped.failure;
|
|
491
|
+
try {
|
|
492
|
+
await observeSessionStop(context.session, bound.session, stopped.textAccepted, bound.plan);
|
|
493
|
+
} catch (refusal) {
|
|
494
|
+
if (undecided(context, refusal)) return unresolved(null);
|
|
495
|
+
if (!(refusal instanceof KitRefusal) || refusal.code !== "conversation_final_text_pending") throw refusal;
|
|
496
|
+
failure ??= { code: refusal.code, detail: refusal.message };
|
|
497
|
+
await observeSessionStop(context.session, bound.session, false, bound.plan);
|
|
498
|
+
}
|
|
499
|
+
const report = stoppedReport(bound);
|
|
500
|
+
if (report !== null) await removeContinuation(context.directory, bound.run);
|
|
501
|
+
try {
|
|
502
|
+
await reportContinuation(context.session, bound.session, {
|
|
503
|
+
providerContinuation: report === null,
|
|
504
|
+
providerCapacityExhausted: stopped.capacityExhausted,
|
|
401
505
|
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
506
|
+
} catch (refusal) {
|
|
507
|
+
if (undecided(context, refusal)) return unresolved(null);
|
|
508
|
+
if (!(refusal instanceof KitRefusal) || refusal.code !== "worker_run_settled") throw refusal;
|
|
509
|
+
await removeContinuation(context.directory, bound.run);
|
|
510
|
+
await removeBinding(context.directory, bound.tmux);
|
|
511
|
+
return { failure, refused: null, settled: true };
|
|
512
|
+
}
|
|
513
|
+
if (report === null) {
|
|
408
514
|
await removeBinding(context.directory, bound.tmux);
|
|
409
|
-
return
|
|
515
|
+
return { failure, refused: null, settled: true };
|
|
410
516
|
}
|
|
411
517
|
const settled = await reportTerminalState(context, bound, report);
|
|
412
|
-
return {
|
|
413
|
-
...outcome,
|
|
414
|
-
reported: settled.reported,
|
|
415
|
-
refused: settled.failure,
|
|
416
|
-
settled: settled.settled,
|
|
417
|
-
};
|
|
518
|
+
return { failure, reported: settled.reported, refused: settled.failure, settled: settled.settled };
|
|
418
519
|
}
|
|
419
520
|
|
|
420
521
|
function assignmentRoute(context, assignment) {
|
|
@@ -425,6 +526,11 @@ function assignmentRoute(context, assignment) {
|
|
|
425
526
|
return assignment.route;
|
|
426
527
|
}
|
|
427
528
|
|
|
529
|
+
async function appliedWorkingCopies(context) {
|
|
530
|
+
await context.workingCopiesApplied?.();
|
|
531
|
+
return workingCopyPaths(context.directory);
|
|
532
|
+
}
|
|
533
|
+
|
|
428
534
|
async function prepareAssignment(context, assignment, resumed) {
|
|
429
535
|
const route = assignmentRoute(context, assignment);
|
|
430
536
|
const command = await installedAgentCommand(context.directory, route.adapter.kind);
|
|
@@ -448,10 +554,14 @@ async function prepareAssignment(context, assignment, resumed) {
|
|
|
448
554
|
ownerStop: null,
|
|
449
555
|
sealed: null,
|
|
450
556
|
reported: null,
|
|
557
|
+
plan: null,
|
|
451
558
|
};
|
|
452
559
|
const authority = { token: assignment.session.token, expiresAt: assignment.session.expiresAt };
|
|
453
|
-
const
|
|
560
|
+
const start = {
|
|
561
|
+
base_instructions: assignment.baseInstructions,
|
|
562
|
+
create_how_we_work: await howWeWorkOwed(context.directory),
|
|
454
563
|
working_directory: assignment.workingDirectory,
|
|
564
|
+
working_copies: await appliedWorkingCopies(context),
|
|
455
565
|
execution_authority: context.configuration.executionAuthority,
|
|
456
566
|
adapter: {
|
|
457
567
|
kind: route.adapter.kind,
|
|
@@ -461,10 +571,16 @@ async function prepareAssignment(context, assignment, resumed) {
|
|
|
461
571
|
effort: route.effort,
|
|
462
572
|
environment: {},
|
|
463
573
|
},
|
|
464
|
-
request:
|
|
574
|
+
request: null,
|
|
465
575
|
provider_session: assignment.providerSessionId ?? null,
|
|
466
|
-
}
|
|
467
|
-
|
|
576
|
+
};
|
|
577
|
+
const opened = await openRunBridge(context, bound, authority, start);
|
|
578
|
+
const request = async () => {
|
|
579
|
+
const token = await sessionAuthority(context.session, bound.session, authority);
|
|
580
|
+
start.request = await localRequest(context.directory, bound.run,
|
|
581
|
+
await readRunRequest(context.session, bound.run, token));
|
|
582
|
+
};
|
|
583
|
+
return { bound, opened, request };
|
|
468
584
|
}
|
|
469
585
|
|
|
470
586
|
async function refuseContinuation(context, command, failure) {
|
|
@@ -502,14 +618,15 @@ async function superviseAssignment(context, assignment, resumed = false) {
|
|
|
502
618
|
detail: refusal instanceof KitRefusal ? refusal.message : null,
|
|
503
619
|
};
|
|
504
620
|
if (resumed) return refuseContinuation(context, assignment, failure);
|
|
505
|
-
if (undecided(refusal)) throw refusal;
|
|
621
|
+
if (undecided(context, refusal)) throw refusal;
|
|
506
622
|
return refuseAssignment(context, assignment, failure);
|
|
507
623
|
}
|
|
508
|
-
const { bound, opened } = prepared;
|
|
624
|
+
const { bound, opened, request } = prepared;
|
|
509
625
|
return superviseBound(context, bound, opened, async (spawn) => {
|
|
510
626
|
if (!resumed) await commitRun(context.session, assignment.run);
|
|
511
627
|
spawn.committed = true;
|
|
512
628
|
await writeBinding(context.directory, bound);
|
|
629
|
+
await request();
|
|
513
630
|
await createSession(context.runtime, {
|
|
514
631
|
session: bound.tmux,
|
|
515
632
|
workingDirectory: assignment.workingDirectory,
|
|
@@ -520,7 +637,7 @@ async function superviseAssignment(context, assignment, resumed = false) {
|
|
|
520
637
|
},
|
|
521
638
|
command: context.launcher,
|
|
522
639
|
});
|
|
523
|
-
await observeSessionStart(context.session, bound.session);
|
|
640
|
+
opened.frames.identify(await observeSessionStart(context.session, bound.session));
|
|
524
641
|
});
|
|
525
642
|
}
|
|
526
643
|
|
|
@@ -531,7 +648,7 @@ async function superviseResumption(context, command) {
|
|
|
531
648
|
}
|
|
532
649
|
return superviseAssignment(context, {
|
|
533
650
|
...command, agentId: held.agentId, workingDirectory: held.workingDirectory,
|
|
534
|
-
conversation: held.conversation, providerSessionId: held.providerSessionId,
|
|
651
|
+
conversation: held.conversation, providerSessionId: held.providerSessionId,
|
|
535
652
|
}, true);
|
|
536
653
|
}
|
|
537
654
|
|
|
@@ -539,7 +656,7 @@ async function superviseRestored(context, binding, authority) {
|
|
|
539
656
|
const opened = await openRunBridge(context, binding, authority);
|
|
540
657
|
return superviseBound(context, binding, opened, async (spawn) => {
|
|
541
658
|
spawn.committed = true;
|
|
542
|
-
await observeSessionStart(context.session, binding.session);
|
|
659
|
+
opened.frames.identify(await observeSessionStart(context.session, binding.session));
|
|
543
660
|
});
|
|
544
661
|
}
|
|
545
662
|
|
|
@@ -551,7 +668,7 @@ async function settleAbsentSession(context, binding) {
|
|
|
551
668
|
providerCapacityExhausted: false,
|
|
552
669
|
});
|
|
553
670
|
} catch (refusal) {
|
|
554
|
-
if (undecided(refusal)) return { settled: false, failure: null };
|
|
671
|
+
if (undecided(context, refusal)) return { settled: false, failure: null };
|
|
555
672
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
556
673
|
}
|
|
557
674
|
await removeContinuation(context.directory, binding.run);
|
|
@@ -567,7 +684,7 @@ async function confirmRestorable(context, binding) {
|
|
|
567
684
|
try {
|
|
568
685
|
authority = await replaceSessionAuthority(context.session, binding.session);
|
|
569
686
|
} catch (refusal) {
|
|
570
|
-
if (undecided(refusal)) return { authority: null, unresolved: true };
|
|
687
|
+
if (undecided(context, refusal)) return { authority: null, unresolved: true };
|
|
571
688
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
572
689
|
await destroySession(context.runtime, binding.tmux);
|
|
573
690
|
await removeBinding(context.directory, binding.tmux);
|
|
@@ -586,20 +703,20 @@ async function reconcile(context) {
|
|
|
586
703
|
}
|
|
587
704
|
const bindings = await readBindings(context.directory);
|
|
588
705
|
if (bindings.length === 0) return { run: null, stopped, unresolved };
|
|
589
|
-
const commands = (await pendingCommands(context))
|
|
706
|
+
const commands = (await pendingCommands(context))?.stops ?? [];
|
|
590
707
|
const carried = [];
|
|
591
708
|
for (const binding of bindings) {
|
|
592
|
-
const resealed = await recoverAssistantSeal(context, binding);
|
|
593
|
-
if (resealed.unresolved) {
|
|
594
|
-
unresolved.push(unresolvedRun(binding, resealed.refused));
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
709
|
if (binding.reported !== null) {
|
|
598
710
|
const settled = await settleReported(context, binding);
|
|
599
711
|
if (settled.settled) await removeBinding(context.directory, binding.tmux);
|
|
600
712
|
else unresolved.push(unresolvedRun(binding, settled.failure));
|
|
601
713
|
continue;
|
|
602
714
|
}
|
|
715
|
+
if (binding.stopped !== null) {
|
|
716
|
+
const settled = await settleStopped(context, binding);
|
|
717
|
+
if (!settled.settled) unresolved.push(unresolvedRun(binding, settled.refused));
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
603
720
|
const command = commands.find((pending) => pending.run === binding.run) ?? null;
|
|
604
721
|
const requestedAt = binding.ownerStop ?? command?.requestedAt ?? null;
|
|
605
722
|
if (requestedAt === null) {
|
|
@@ -639,7 +756,7 @@ async function reportHeldSessions(context) {
|
|
|
639
756
|
const continued = (await readContinuationRuns(context.directory)).map((run) => ({ run, reported: null }));
|
|
640
757
|
const held = new Map();
|
|
641
758
|
for (const entry of [...settlements, ...bindings, ...continued]) {
|
|
642
|
-
held.set(entry.run, held.get(entry.run) ?? entry.reported);
|
|
759
|
+
held.set(entry.run, held.get(entry.run) ?? entry.reported ?? stoppedReport(entry));
|
|
643
760
|
}
|
|
644
761
|
const reported = await reportSessions(
|
|
645
762
|
context.session,
|
|
@@ -666,39 +783,55 @@ async function afterEpochReport(context, pass) {
|
|
|
666
783
|
return pass();
|
|
667
784
|
}
|
|
668
785
|
|
|
669
|
-
|
|
670
|
-
|
|
786
|
+
function unlessDraining(context, act) {
|
|
787
|
+
return context.update?.requested ? null : act();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function workerPass(context, dependencies, baseConfiguration, reach) {
|
|
791
|
+
const { session, signal } = context;
|
|
671
792
|
const reconciled = await reconcile(context);
|
|
672
|
-
if (reconciled.run !== null || reconciled.unresolved.length > 0)
|
|
673
|
-
|
|
674
|
-
|
|
793
|
+
if (reconciled.run !== null || reconciled.unresolved.length > 0 || context.update?.requested) {
|
|
794
|
+
return reconciled;
|
|
795
|
+
}
|
|
796
|
+
context.busy = false;
|
|
797
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, baseConfiguration);
|
|
798
|
+
context.startLogins?.(needsLogin);
|
|
799
|
+
context.busy = true;
|
|
800
|
+
const { configuration } = context;
|
|
801
|
+
context.inbound = await unlessDraining(context, () => reconcileInbound(context.directory, session));
|
|
802
|
+
const setup = await unlessDraining(context, () => acquireSetup(session, signal));
|
|
675
803
|
if (setup !== null) {
|
|
676
804
|
context.setup = await runSetup(
|
|
677
|
-
{
|
|
805
|
+
{
|
|
806
|
+
session,
|
|
807
|
+
configuration,
|
|
808
|
+
directory: context.directory,
|
|
809
|
+
dependencies,
|
|
810
|
+
signal,
|
|
811
|
+
waitAgentLogin: context.waitAgentLogin,
|
|
812
|
+
},
|
|
678
813
|
setup.request,
|
|
679
814
|
);
|
|
815
|
+
await context.recordSetupLogins?.(context.setup);
|
|
816
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
817
|
+
context.startLogins?.(needsLogin);
|
|
680
818
|
}
|
|
681
|
-
|
|
682
|
-
{ directory: context.directory, at: dependencies.now?.() },
|
|
683
|
-
configuration,
|
|
684
|
-
dependencies,
|
|
685
|
-
);
|
|
686
|
-
const readiness = await agentReadiness({ directory: context.directory }, configuration, dependencies);
|
|
687
|
-
await reportAgentCapabilities(session, readiness.agents);
|
|
688
|
-
if (readiness.refused !== null) throw readiness.refused;
|
|
689
|
-
const resumption = await acquireOneResumption(session);
|
|
819
|
+
const resumption = await unlessDraining(context, () => acquireOneResumption(session));
|
|
690
820
|
if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
|
|
691
|
-
|
|
821
|
+
if (reach !== "runs") return reconciled;
|
|
822
|
+
const assignment = await unlessDraining(context, () => acquireOneRun(session));
|
|
692
823
|
if (assignment === null) return reconciled;
|
|
693
824
|
return { ...reconciled, run: await superviseAssignment(context, assignment) };
|
|
694
825
|
}
|
|
695
826
|
|
|
696
|
-
async function workerContext(options, dependencies) {
|
|
827
|
+
async function workerContext(options, dependencies, resident = false) {
|
|
697
828
|
const configuration = await readConfiguration(options.configuration);
|
|
698
829
|
const runtime = workerRuntime(options.directory, dependencies.tmux ?? "tmux");
|
|
699
830
|
const launcher = dependencies.launcher ?? [process.execPath, LAUNCHER_MAIN];
|
|
700
831
|
const tmux = await readyTmux(runtime);
|
|
701
|
-
const session =
|
|
832
|
+
const session = resident
|
|
833
|
+
? await residentSession(options, dependencies)
|
|
834
|
+
: await openSession({ ...options, acquire: true }, dependencies);
|
|
702
835
|
const context = {
|
|
703
836
|
configuration,
|
|
704
837
|
runtime,
|
|
@@ -707,24 +840,97 @@ async function workerContext(options, dependencies) {
|
|
|
707
840
|
launchBase: launchBase(dependencies),
|
|
708
841
|
container: dependencies.container ?? process.env.HOUSE_KIT_CONTAINER === "1",
|
|
709
842
|
directory: options.directory,
|
|
843
|
+
home: dependencies.home ?? homedir(),
|
|
710
844
|
files: { originals: [], failures: [] },
|
|
845
|
+
commands: residentWake(),
|
|
846
|
+
stream: kitStreamWriter(),
|
|
711
847
|
epochReport: null,
|
|
848
|
+
agentRefresh: Promise.resolve(),
|
|
849
|
+
configurationPath: options.configuration,
|
|
850
|
+
busy: false,
|
|
851
|
+
deferredRemoval: false,
|
|
712
852
|
};
|
|
713
853
|
return { context, tmux };
|
|
714
854
|
}
|
|
715
855
|
|
|
716
|
-
async function
|
|
717
|
-
|
|
856
|
+
async function refreshAgentCapabilities(context, dependencies, signal, configuration, pointer = false) {
|
|
857
|
+
const base = configuration ?? (await readConfiguration(context.configurationPath));
|
|
858
|
+
const busy = context.busy || (await readBindings(context.directory)).length > 0;
|
|
859
|
+
const result = await reconcileAgentKinds(
|
|
860
|
+
{ directory: context.directory },
|
|
861
|
+
context.session,
|
|
862
|
+
base,
|
|
863
|
+
dependencies,
|
|
864
|
+
signal,
|
|
865
|
+
{
|
|
866
|
+
pointer,
|
|
867
|
+
busy,
|
|
868
|
+
lastDesired: context.lastDesired,
|
|
869
|
+
onDesired: (wanted) => context.cancelLogins?.(wanted),
|
|
870
|
+
},
|
|
871
|
+
);
|
|
872
|
+
context.deferredRemoval = result.retained.length > 0;
|
|
873
|
+
context.configuration = result.selected;
|
|
874
|
+
context.agents = result.updated;
|
|
875
|
+
context.lastDesired = [...result.selected.agents];
|
|
876
|
+
if (result.refused !== null) throw result.refused;
|
|
877
|
+
return result.reported.filter(
|
|
878
|
+
(agent) =>
|
|
879
|
+
result.selected.agents.includes(agent.kind) && agent.release !== null && !agent.signed_in,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function queueAgentRefresh(context, dependencies, signal, configuration, pointer = false) {
|
|
884
|
+
const refreshed = context.agentRefresh.then(() =>
|
|
885
|
+
refreshAgentCapabilities(context, dependencies, signal, configuration, pointer),
|
|
886
|
+
);
|
|
887
|
+
context.agentRefresh = refreshed.catch(() => {});
|
|
888
|
+
return refreshed;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function controlledPass(context, dependencies, configuration, signal, reach = "runs") {
|
|
718
892
|
context.signal = signal;
|
|
719
893
|
context.session.signal = signal;
|
|
720
|
-
|
|
894
|
+
await reconcileSkillSet({
|
|
895
|
+
directory: context.directory,
|
|
896
|
+
home: context.home,
|
|
897
|
+
accepted: configuration.skills,
|
|
898
|
+
});
|
|
899
|
+
context.busy = true;
|
|
900
|
+
try {
|
|
901
|
+
return await afterEpochReport(context, () => workerPass(context, dependencies, configuration, reach));
|
|
902
|
+
} finally {
|
|
903
|
+
context.busy = false;
|
|
904
|
+
if (context.deferredRemoval) context.onIdle?.();
|
|
905
|
+
}
|
|
721
906
|
}
|
|
722
907
|
|
|
723
908
|
export async function runWorkers(options, dependencies = {}) {
|
|
724
909
|
const { context, tmux } = await workerContext(options, dependencies);
|
|
725
910
|
const { session, runtime } = context;
|
|
726
911
|
const supervised = await maintainControl(session, async (signal) => {
|
|
727
|
-
|
|
912
|
+
const watcher = new AbortController();
|
|
913
|
+
const commanded = (subject) => {
|
|
914
|
+
if (subject === null || subject === "commands") context.commands.notify();
|
|
915
|
+
};
|
|
916
|
+
const watching = (dependencies.watchKitWork ?? watchKitWork)(
|
|
917
|
+
session,
|
|
918
|
+
AbortSignal.any([signal, watcher.signal]),
|
|
919
|
+
{
|
|
920
|
+
available: commanded,
|
|
921
|
+
changed: () => {},
|
|
922
|
+
opened: context.stream.opened,
|
|
923
|
+
closed: context.stream.closed,
|
|
924
|
+
},
|
|
925
|
+
dependencies,
|
|
926
|
+
).catch((error) => reported(`control stream ended: ${error?.code ?? error?.message ?? error}`));
|
|
927
|
+
try {
|
|
928
|
+
await reportedWorkingCopies(applyWorkingCopies, context, signal);
|
|
929
|
+
return await controlledPass(context, dependencies, context.configuration, signal);
|
|
930
|
+
} finally {
|
|
931
|
+
watcher.abort();
|
|
932
|
+
await watching;
|
|
933
|
+
}
|
|
728
934
|
}, session.signal);
|
|
729
935
|
return {
|
|
730
936
|
tmux,
|
|
@@ -740,17 +946,312 @@ export async function runWorkers(options, dependencies = {}) {
|
|
|
740
946
|
};
|
|
741
947
|
}
|
|
742
948
|
|
|
949
|
+
function reported(line) {
|
|
950
|
+
process.stderr.write(`kit: ${line}\n`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function reportedWorkingCopies(act, context, signal) {
|
|
954
|
+
const outcome = await act(context, signal);
|
|
955
|
+
for (const refused of outcome.refused) {
|
|
956
|
+
reported(`working copy ${refused.path} refused: ${refused.code}`);
|
|
957
|
+
}
|
|
958
|
+
return outcome;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export function residentUpdate(context, wake) {
|
|
962
|
+
let requested = existsSync(updateDrain(context.directory));
|
|
963
|
+
let ready = null;
|
|
964
|
+
const waiting = [];
|
|
965
|
+
return {
|
|
966
|
+
get requested() {
|
|
967
|
+
return requested;
|
|
968
|
+
},
|
|
969
|
+
async request(serving) {
|
|
970
|
+
let heard = null;
|
|
971
|
+
let hear;
|
|
972
|
+
const answered = new Promise((resolve) => {
|
|
973
|
+
hear = (answer) => {
|
|
974
|
+
heard = answer;
|
|
975
|
+
resolve(answer);
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
waiting.push(hear);
|
|
979
|
+
if (!requested) {
|
|
980
|
+
requested = true;
|
|
981
|
+
await writeText(updateDrain(context.directory), "");
|
|
982
|
+
}
|
|
983
|
+
wake.notify();
|
|
984
|
+
const sessions = await heldSessions(context);
|
|
985
|
+
if (heard !== null) return heard;
|
|
986
|
+
if (ready === null && !serving && sessions.length === 0) return answered;
|
|
987
|
+
waiting.splice(waiting.indexOf(hear), 1);
|
|
988
|
+
if (ready !== null) return ready;
|
|
989
|
+
if (serving) return { ready: false, sessions, serving };
|
|
990
|
+
return { ready: false, sessions };
|
|
991
|
+
},
|
|
992
|
+
answer(answer) {
|
|
993
|
+
for (const resolve of waiting.splice(0)) resolve(answer);
|
|
994
|
+
},
|
|
995
|
+
async announce() {
|
|
996
|
+
if (waiting.length === 0) return;
|
|
997
|
+
const sessions = await heldSessions(context);
|
|
998
|
+
if (sessions.length > 0) this.answer({ ready: false, sessions });
|
|
999
|
+
},
|
|
1000
|
+
quiesce() {
|
|
1001
|
+
ready = { ready: true, sessions: [] };
|
|
1002
|
+
this.answer(ready);
|
|
1003
|
+
},
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async function heldSessions(context) {
|
|
1008
|
+
const bindings = await readBindings(context.directory);
|
|
1009
|
+
if (!context.session.refused) {
|
|
1010
|
+
return [...bindings, ...(await readSettlements(context.directory))].map((held) => held.run);
|
|
1011
|
+
}
|
|
1012
|
+
const live = [];
|
|
1013
|
+
for (const binding of bindings) {
|
|
1014
|
+
if (binding.stopped === null && (await sessionPresent(context.runtime, binding.tmux))) {
|
|
1015
|
+
live.push(binding.run);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return live;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function residentSession(options, dependencies) {
|
|
1022
|
+
for (;;) {
|
|
1023
|
+
try {
|
|
1024
|
+
return await openSession({ ...options, resident: true }, dependencies);
|
|
1025
|
+
} catch (refusal) {
|
|
1026
|
+
if (refusal?.code !== "environment_control_lease_active") throw refusal;
|
|
1027
|
+
await controlSleep(dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS, options.signal);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
743
1032
|
export async function runResident(options, dependencies = {}) {
|
|
744
|
-
const { context } = await workerContext(options, dependencies);
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1033
|
+
const { context } = await workerContext(options, dependencies, true);
|
|
1034
|
+
const { session } = context;
|
|
1035
|
+
const interval = dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS;
|
|
1036
|
+
const wake = residentWake();
|
|
1037
|
+
const update = residentUpdate(context, wake);
|
|
1038
|
+
context.update = update;
|
|
1039
|
+
let sources = null;
|
|
1040
|
+
const served = await serveResident(
|
|
1041
|
+
session,
|
|
1042
|
+
context.directory,
|
|
1043
|
+
options.signal,
|
|
1044
|
+
(serving) => update.request(serving),
|
|
1045
|
+
(request, signal) =>
|
|
1046
|
+
pushWorkingCopy({ directory: context.directory, session }, request, ownerCaller(session), signal).catch(
|
|
1047
|
+
(error) => ({ refused: true, text: formatCommandError(error) }),
|
|
1048
|
+
),
|
|
1049
|
+
(source) =>
|
|
1050
|
+
sources === null ? Promise.reject(new KitRefusal("kit_resident_unavailable")) : sources.capture(source),
|
|
1051
|
+
context.stream,
|
|
1052
|
+
);
|
|
1053
|
+
const serve = async (signal) => {
|
|
1054
|
+
context.busy = true;
|
|
1055
|
+
const watcher = new AbortController();
|
|
1056
|
+
let failure = null;
|
|
1057
|
+
const fail = (error) => {
|
|
1058
|
+
if (signal.aborted || session.refused) return;
|
|
1059
|
+
failure = error;
|
|
1060
|
+
wake.notify();
|
|
1061
|
+
};
|
|
1062
|
+
const logins = new Map();
|
|
1063
|
+
const attemptedLogins = new Map();
|
|
1064
|
+
context.cancelLogins = (wanted) => {
|
|
1065
|
+
for (const [kind, running] of logins) {
|
|
1066
|
+
if (!wanted.includes(kind)) running.controller.abort();
|
|
1067
|
+
}
|
|
1068
|
+
for (const kind of attemptedLogins.keys()) {
|
|
1069
|
+
if (!wanted.includes(kind)) attemptedLogins.delete(kind);
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
context.recordSetupLogins = async (modules) => {
|
|
1073
|
+
for (const module of modules) {
|
|
1074
|
+
if (module.category !== "agent" || module.status !== "needs_attention") continue;
|
|
1075
|
+
const release = (await readAgentState(context.directory, module.name)).release;
|
|
1076
|
+
if (release !== null) attemptedLogins.set(module.name, release);
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
context.startLogins = (agents) => {
|
|
1080
|
+
if (update.requested) return;
|
|
1081
|
+
for (const agent of agents) {
|
|
1082
|
+
const { kind, release } = agent;
|
|
1083
|
+
if (logins.has(kind) || attemptedLogins.get(kind) === release) continue;
|
|
1084
|
+
const controller = new AbortController();
|
|
1085
|
+
const loginSignal = AbortSignal.any([signal, controller.signal]);
|
|
1086
|
+
const task = (async () => {
|
|
1087
|
+
if ((await acquireSetup(context.session, loginSignal)) !== null) return;
|
|
1088
|
+
attemptedLogins.set(kind, release);
|
|
1089
|
+
await signInWantedAgent({ ...context, dependencies, signal: loginSignal }, kind);
|
|
1090
|
+
await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
1091
|
+
})()
|
|
1092
|
+
.catch((error) => {
|
|
1093
|
+
if (loginSignal.aborted) return;
|
|
1094
|
+
fail(error);
|
|
1095
|
+
})
|
|
1096
|
+
.finally(() => logins.delete(kind));
|
|
1097
|
+
logins.set(kind, { controller, task });
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
context.waitAgentLogin = async (kind) => {
|
|
1101
|
+
await logins.get(kind)?.task;
|
|
1102
|
+
};
|
|
1103
|
+
const agentRefresh = coalescedTask(signal, fail, async () => {
|
|
1104
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
1105
|
+
context.startLogins(needsLogin);
|
|
1106
|
+
});
|
|
1107
|
+
const unavailable = (error) => reported(`working copies unavailable: ${error?.message ?? error}`);
|
|
1108
|
+
const changed = () => {
|
|
1109
|
+
if (!update.requested) deliveries.trigger();
|
|
1110
|
+
};
|
|
1111
|
+
const behind = (outcome) => {
|
|
1112
|
+
if (!outcome.behind) return;
|
|
1113
|
+
controlSleep(interval, signal).then(changed, () => {});
|
|
1114
|
+
};
|
|
1115
|
+
const copies = coalescedTask(signal, unavailable, async () =>
|
|
1116
|
+
behind(await reportedWorkingCopies(applyWorkingCopies, context, signal)),
|
|
1117
|
+
);
|
|
1118
|
+
const deliveries = coalescedTask(signal, unavailable, async () =>
|
|
1119
|
+
behind(await reportedWorkingCopies(deliverWorkingCopies, context, signal)),
|
|
1120
|
+
);
|
|
1121
|
+
sources = sourceEntries({
|
|
1122
|
+
session: context.session,
|
|
1123
|
+
directory: context.directory,
|
|
1124
|
+
dependencies,
|
|
1125
|
+
signal: AbortSignal.any([signal, watcher.signal]),
|
|
1126
|
+
now: session.now,
|
|
1127
|
+
draining: () => update.requested,
|
|
1128
|
+
unavailable: (error) => reported(`google sources unavailable: ${error?.message ?? error}`),
|
|
1129
|
+
});
|
|
1130
|
+
await sources.start();
|
|
1131
|
+
const pointer = () => {
|
|
1132
|
+
if (!update.requested) agentRefresh.trigger();
|
|
1133
|
+
};
|
|
1134
|
+
let passOwed = null;
|
|
1135
|
+
let deferred = null;
|
|
1136
|
+
let retrying = null;
|
|
1137
|
+
const higher = (reach, than) =>
|
|
1138
|
+
PASS_REACHES.indexOf(reach) > PASS_REACHES.indexOf(than) ? reach : than;
|
|
1139
|
+
const owe = (reach) => {
|
|
1140
|
+
passOwed = higher(reach, passOwed);
|
|
1141
|
+
wake.notify();
|
|
1142
|
+
};
|
|
1143
|
+
const defer = (reach) => {
|
|
1144
|
+
deferred = higher(reach, deferred);
|
|
1145
|
+
if (retrying !== null) return;
|
|
1146
|
+
retrying = new AbortController();
|
|
1147
|
+
controlSleep(interval, AbortSignal.any([signal, retrying.signal])).then(() => {
|
|
1148
|
+
retrying = null;
|
|
1149
|
+
const owed = deferred;
|
|
1150
|
+
deferred = null;
|
|
1151
|
+
owe(owed);
|
|
1152
|
+
}, () => {});
|
|
1153
|
+
};
|
|
1154
|
+
const settle = (reach) => {
|
|
1155
|
+
retrying?.abort();
|
|
1156
|
+
retrying = null;
|
|
1157
|
+
if (higher(deferred, reach) !== reach) owe(deferred);
|
|
1158
|
+
deferred = null;
|
|
1159
|
+
};
|
|
1160
|
+
const available = (subject) => {
|
|
1161
|
+
if (subject === null || subject === "runs") owe("runs");
|
|
1162
|
+
if (subject === null || subject === "commands") {
|
|
1163
|
+
context.commands.notify();
|
|
1164
|
+
owe("commands");
|
|
1165
|
+
}
|
|
1166
|
+
if (update.requested) return;
|
|
1167
|
+
if (subject === null || subject === "agents") agentRefresh.trigger();
|
|
1168
|
+
if (subject === null || subject === "working_copy") copies.trigger();
|
|
1169
|
+
if (subject === null || subject === "sources") sources.reconcile();
|
|
1170
|
+
};
|
|
1171
|
+
context.onIdle = pointer;
|
|
1172
|
+
context.workingCopiesApplied = copies.settled;
|
|
1173
|
+
const watching = (dependencies.watchKitWork ?? watchKitWork)(
|
|
1174
|
+
context.session,
|
|
1175
|
+
AbortSignal.any([signal, watcher.signal]),
|
|
1176
|
+
{
|
|
1177
|
+
available,
|
|
1178
|
+
changed,
|
|
1179
|
+
opened: context.stream.opened,
|
|
1180
|
+
closed: context.stream.closed,
|
|
1181
|
+
viewed: (frame) => served.forward(frame),
|
|
1182
|
+
},
|
|
1183
|
+
dependencies,
|
|
1184
|
+
).catch(fail);
|
|
1185
|
+
try {
|
|
1186
|
+
while (!signal.aborted) {
|
|
1187
|
+
if (failure !== null) throw failure;
|
|
1188
|
+
if (passOwed !== null && !session.refused) {
|
|
1189
|
+
const reach = passOwed;
|
|
1190
|
+
passOwed = null;
|
|
1191
|
+
const configuration = await readConfiguration(options.configuration);
|
|
1192
|
+
const passed = await controlledPass(context, dependencies, configuration, signal, reach).catch(
|
|
1193
|
+
(error) => {
|
|
1194
|
+
if (!session.refused) throw error;
|
|
1195
|
+
return null;
|
|
1196
|
+
},
|
|
1197
|
+
);
|
|
1198
|
+
if ((passed?.run ?? null) !== null) owe(reach);
|
|
1199
|
+
if ((passed?.unresolved.length ?? 0) > 0) defer(reach);
|
|
1200
|
+
else if (passed !== null) settle(reach);
|
|
1201
|
+
}
|
|
1202
|
+
const draining = session.refused || update.requested;
|
|
1203
|
+
if (draining) {
|
|
1204
|
+
const sessions = await heldSessions(context);
|
|
1205
|
+
if (sessions.length > 0) update.answer({ ready: false, sessions });
|
|
1206
|
+
else if (
|
|
1207
|
+
session.refused ||
|
|
1208
|
+
(!served.serving() &&
|
|
1209
|
+
logins.size === 0 &&
|
|
1210
|
+
agentRefresh.idle() &&
|
|
1211
|
+
copies.idle() &&
|
|
1212
|
+
deliveries.idle() &&
|
|
1213
|
+
sources.idle())
|
|
1214
|
+
) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (session.refused || passOwed === null) await wake.wait(draining ? interval : Infinity, signal);
|
|
1219
|
+
}
|
|
1220
|
+
} finally {
|
|
1221
|
+
watcher.abort();
|
|
1222
|
+
await watching;
|
|
1223
|
+
await agentRefresh.settled();
|
|
1224
|
+
await copies.settled();
|
|
1225
|
+
await deliveries.settled();
|
|
1226
|
+
await sources.settled();
|
|
1227
|
+
for (const running of logins.values()) running.controller.abort();
|
|
1228
|
+
await Promise.all([...logins.values()].map((running) => running.task));
|
|
1229
|
+
await context.agentRefresh;
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
try {
|
|
1233
|
+
for (;;) {
|
|
749
1234
|
try {
|
|
750
|
-
await
|
|
1235
|
+
await maintainControl(session, serve, options.signal, { release: false });
|
|
751
1236
|
} catch (error) {
|
|
752
|
-
|
|
1237
|
+
await releaseControl(session);
|
|
1238
|
+
throw error;
|
|
1239
|
+
}
|
|
1240
|
+
if (session.refused) break;
|
|
1241
|
+
try {
|
|
1242
|
+
await releaseControl(session, { required: true });
|
|
1243
|
+
break;
|
|
1244
|
+
} catch (refusal) {
|
|
1245
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
1246
|
+
if (session.refused) break;
|
|
1247
|
+
update.answer({ ready: false, sessions: [], refused: refusal.code });
|
|
753
1248
|
}
|
|
1249
|
+
await wake.wait(interval, options.signal);
|
|
754
1250
|
}
|
|
755
|
-
|
|
1251
|
+
update.quiesce();
|
|
1252
|
+
while (!options.signal.aborted) await wake.wait(Infinity, options.signal);
|
|
1253
|
+
throw new KitRefusal("kit_command_cancelled");
|
|
1254
|
+
} finally {
|
|
1255
|
+
await served.close();
|
|
1256
|
+
}
|
|
756
1257
|
}
|