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