@agentshouse/kit 0.1.0-alpha.16 → 0.1.0-alpha.18

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/src/house.js CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  MCP_PROTOCOL_VERSION,
30
30
  WORKER_RUN_TOKEN_MARGIN_MS,
31
31
  runOutcomePath,
32
+ runRequestPath,
32
33
  secretInputPath,
33
34
  SECRET_VALUE_MAX,
34
35
  sessionInteractionsPath,
@@ -171,7 +172,7 @@ async function residentRelay(session, request, signal, draining) {
171
172
  }
172
173
  }
173
174
 
174
- export async function serveResident(session, directory, signal, drain) {
175
+ export async function serveResident(session, directory, signal, drain, push) {
175
176
  const socket = residentSocket(directory);
176
177
  await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
177
178
  await rm(socket, { force: true });
@@ -228,12 +229,15 @@ export async function serveResident(session, directory, signal, drain) {
228
229
  }
229
230
  const cancelled = new AbortController();
230
231
  relayed.set(request.id, cancelled);
231
- void residentRelay(session, request, AbortSignal.any([signal, cancelled.signal]), draining).then(
232
- (answer) => {
233
- relayed.delete(request.id);
234
- if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
235
- },
236
- );
232
+ const relaySignal = AbortSignal.any([signal, cancelled.signal]);
233
+ const answered =
234
+ request.push === undefined
235
+ ? residentRelay(session, request, relaySignal, draining)
236
+ : push(request.push, relaySignal).then((pushed) => ({ pushed }));
237
+ void answered.then((answer) => {
238
+ relayed.delete(request.id);
239
+ if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
240
+ });
237
241
  },
238
242
  onRefusal: () => connection.destroy(),
239
243
  });
@@ -301,45 +305,54 @@ async function residentDelegation(directory, fetcher) {
301
305
  }
302
306
  connection.unref();
303
307
  let next = 0;
308
+ function exchange(frame, signal) {
309
+ signal?.throwIfAborted();
310
+ if (connection.destroyed) return Promise.resolve(unavailable());
311
+ return new Promise((resolve, reject) => {
312
+ const id = (next += 1);
313
+ const cancel = () => {
314
+ pending.delete(id);
315
+ settle(id);
316
+ if (!connection.destroyed) writeFrame(connection, { id, cancel: true });
317
+ reject(signal.reason);
318
+ };
319
+ signal?.addEventListener("abort", cancel, { once: true });
320
+ pending.set(id, (answer) => {
321
+ signal?.removeEventListener("abort", cancel);
322
+ resolve(answer);
323
+ });
324
+ connection.ref();
325
+ writeFrame(connection, { id, ...frame });
326
+ });
327
+ }
328
+ async function push(request, signal) {
329
+ const answer = await exchange({ push: request }, signal);
330
+ if (answer.pushed === undefined) throw new KitRefusal("kit_resident_unavailable");
331
+ return answer.pushed;
332
+ }
304
333
  async function delegated(url, init = {}) {
305
334
  const { authorization, ...headers } = init.headers ?? {};
306
335
  if (authorization !== RESIDENT) return fetcher(url, init);
307
- init.signal?.throwIfAborted();
308
336
  const address = new URL(url);
309
- const answer = connection.destroyed
310
- ? unavailable()
311
- : await new Promise((resolve, reject) => {
312
- const id = (next += 1);
313
- const cancel = () => {
314
- pending.delete(id);
315
- settle(id);
316
- if (!connection.destroyed) writeFrame(connection, { id, cancel: true });
317
- reject(init.signal.reason);
318
- };
319
- init.signal?.addEventListener("abort", cancel, { once: true });
320
- pending.set(id, (answer) => {
321
- init.signal?.removeEventListener("abort", cancel);
322
- resolve(answer);
323
- });
324
- connection.ref();
325
- writeFrame(connection, {
326
- id,
327
- method: init.method ?? "GET",
328
- path: `${address.pathname}${address.search}`,
329
- headers,
330
- ...(typeof init.body === "string"
331
- ? { text: init.body }
332
- : init.body === undefined
333
- ? {}
334
- : { bytes: Buffer.from(init.body).toString("base64") }),
335
- });
336
- });
337
+ const answer = await exchange(
338
+ {
339
+ method: init.method ?? "GET",
340
+ path: `${address.pathname}${address.search}`,
341
+ headers,
342
+ ...(typeof init.body === "string"
343
+ ? { text: init.body }
344
+ : init.body === undefined
345
+ ? {}
346
+ : { bytes: Buffer.from(init.body).toString("base64") }),
347
+ },
348
+ init.signal,
349
+ );
337
350
  return new Response(answer.body === null ? null : Buffer.from(answer.body, "base64"), {
338
351
  status: answer.status,
339
352
  headers: answer.type === null ? {} : { "content-type": answer.type },
340
353
  });
341
354
  }
342
- return { connection, fetch: delegated };
355
+ return { connection, fetch: delegated, push };
343
356
  }
344
357
 
345
358
  export async function openSession(options, dependencies = {}) {
@@ -358,6 +371,7 @@ export async function openSession(options, dependencies = {}) {
358
371
  : await residentDelegation(options.directory, dependencies.fetch ?? fetch);
359
372
  if (resident !== null) {
360
373
  session.resident = resident.connection;
374
+ session.push = resident.push;
361
375
  session.dependencies = { ...dependencies, fetch: resident.fetch };
362
376
  return session;
363
377
  }
@@ -734,11 +748,7 @@ export async function reportLogin(session, login, outcome, signal = session.sign
734
748
  }
735
749
 
736
750
  const envelope = declarationReader("kit_acquire_rejected");
737
-
738
- function routineInput(value, declaration) {
739
- if (typeof value !== "string" || Array.from(value).length > 20_000) envelope.refuse(declaration);
740
- return value;
741
- }
751
+ const input = declarationReader("kit_run_input_unreadable");
742
752
 
743
753
  function acquiredRoute(value, declaration) {
744
754
  const route = envelope.plain(value, declaration);
@@ -756,79 +766,82 @@ function acquiredRoute(value, declaration) {
756
766
 
757
767
  function conversationText(value, declaration) {
758
768
  if (value === null) return null;
759
- if (typeof value !== "string" || value.length > 4096) envelope.refuse(declaration);
769
+ if (typeof value !== "string" || value.length > 4096) input.refuse(declaration);
760
770
  return value;
761
771
  }
762
772
 
763
773
  function conversationMessage(value, declaration) {
764
- const message = envelope.plain(value, declaration);
774
+ const message = input.plain(value, declaration);
765
775
  const text = conversationText(message.text, `${declaration}.text`);
766
776
  const caption = conversationText(message.caption, `${declaration}.caption`);
767
- if (!Array.isArray(message.files)) envelope.refuse(`${declaration}.files`);
777
+ if (!Array.isArray(message.files)) input.refuse(`${declaration}.files`);
768
778
  const files = message.files.map((value, index) => {
769
779
  const name = `${declaration}.files[${index}]`;
770
- const file = envelope.plain(value, name);
780
+ const file = input.plain(value, name);
771
781
  if (file.bytes !== null && (!Number.isSafeInteger(file.bytes) || file.bytes < 0)) {
772
- envelope.refuse(`${name}.bytes`);
782
+ input.refuse(`${name}.bytes`);
773
783
  }
774
784
  return {
775
- path: envelope.text(file.path, `${name}.path`, 2048),
776
- media_kind: envelope.text(file.media_kind, `${name}.media_kind`, 128),
777
- media_type: file.media_type === null ? null : envelope.text(file.media_type, `${name}.media_type`, 255),
785
+ path: input.text(file.path, `${name}.path`, 2048),
786
+ media_kind: input.text(file.media_kind, `${name}.media_kind`, 128),
787
+ media_type: file.media_type === null ? null : input.text(file.media_type, `${name}.media_type`, 255),
778
788
  bytes: file.bytes,
779
789
  };
780
790
  });
781
- if (!text && !caption && files.length === 0) envelope.refuse(declaration);
791
+ if (!text && !caption && files.length === 0) input.refuse(declaration);
782
792
  return {
783
- message_id: envelope.text(message.message_id, `${declaration}.message_id`, 128),
793
+ message_id: input.text(message.message_id, `${declaration}.message_id`, 128),
784
794
  text, caption, files,
785
795
  };
786
796
  }
787
797
 
788
- function acquiredRequest(acquired, declaration) {
789
- if (acquired.kind === "conversation") {
790
- if (acquired.origin !== "user") envelope.refuse(`${declaration}.origin`);
791
- const conversation = envelope.plain(acquired.conversation, `${declaration}.conversation`);
792
- const turn = envelope.plain(conversation.turn, `${declaration}.conversation.turn`);
798
+ function runRequest(answer) {
799
+ const request = input.plain(answer, "request");
800
+ if (request.kind === "conversation") {
801
+ if (request.origin !== "user") input.refuse("request.origin");
802
+ const turn = input.plain(request.turn, "request.turn");
793
803
  return {
804
+ kind: "conversation",
794
805
  origin: "user",
795
- conversation: {
796
- turn: {
797
- id: envelope.text(turn.id, `${declaration}.conversation.turn.id`, 128),
798
- sequence: envelope.positiveCount(turn.sequence, `${declaration}.conversation.turn.sequence`),
799
- messages: envelope.filled(turn.messages, `${declaration}.conversation.turn.messages`)
800
- .map((message, index) => conversationMessage(message, `${declaration}.conversation.turn.messages[${index}]`)),
801
- },
806
+ turn: {
807
+ id: input.text(turn.id, "request.turn.id", 128),
808
+ sequence: input.positiveCount(turn.sequence, "request.turn.sequence"),
809
+ messages: input.filled(turn.messages, "request.turn.messages")
810
+ .map((message, index) => conversationMessage(message, `request.turn.messages[${index}]`)),
802
811
  },
803
812
  };
804
813
  }
805
- if (acquired.kind !== "routine") envelope.refuse(`${declaration}.kind`);
806
- const request = envelope.plain(acquired.request, `${declaration}.request`);
807
- const routine = envelope.plain(acquired.routine, `${declaration}.routine`);
814
+ if (request.kind !== "routine") input.refuse("request.kind");
815
+ const routine = input.plain(request.routine, "request.routine");
816
+ if (typeof request.payload !== "string" || Array.from(request.payload).length > 20_000) input.refuse("request.payload");
817
+ if (!Array.isArray(request.references)) input.refuse("request.references");
808
818
  return {
809
- origin: envelope.text(acquired.origin, `${declaration}.origin`, 32),
819
+ kind: "routine",
820
+ origin: input.text(request.origin, "request.origin", 32),
810
821
  routine: {
811
- name: envelope.text(routine.name, `${declaration}.routine.name`, 256),
812
- version: envelope.positiveCount(routine.version, `${declaration}.routine.version`),
822
+ name: input.text(routine.name, "request.routine.name", 256),
823
+ version: input.positiveCount(routine.version, "request.routine.version"),
813
824
  },
814
- payload: routineInput(request.payload, `${declaration}.request.payload`),
815
- requested_by: envelope.text(
816
- request.requested_by,
817
- `${declaration}.request.requested_by`,
818
- 128,
819
- ),
820
- requested_at: envelope.text(
821
- request.requested_at,
822
- `${declaration}.request.requested_at`,
823
- 64,
824
- ),
825
- deliver_at:
826
- request.deliver_at === null
827
- ? null
828
- : envelope.text(request.deliver_at, `${declaration}.request.deliver_at`, 64),
825
+ instruction: input.text(request.instruction, "request.instruction", 256 * 1024),
826
+ payload: request.payload,
827
+ references: request.references.map((reference, index) => input.plain(reference, `request.references[${index}]`)),
828
+ requested_by: input.text(request.requested_by, "request.requested_by", 128),
829
+ requested_at: input.text(request.requested_at, "request.requested_at", 64),
830
+ deliver_at: request.deliver_at === null ? null : input.text(request.deliver_at, "request.deliver_at", 64),
829
831
  };
830
832
  }
831
833
 
834
+ export async function readRunRequest(session, run, token, signal = session.signal) {
835
+ return runRequest(await houseRequest(
836
+ session.house,
837
+ `Bearer ${token}`,
838
+ runRequestPath(run),
839
+ {},
840
+ session.dependencies,
841
+ signal,
842
+ ));
843
+ }
844
+
832
845
  function acquiredAssignment(value, declaration) {
833
846
  const acquired = envelope.plain(value, declaration);
834
847
  const bridge = envelope.plain(acquired.session, `${declaration}.session`);
@@ -857,7 +870,6 @@ function acquiredAssignment(value, declaration) {
857
870
  token: envelope.text(bridge.bridge_token, `${declaration}.session.bridge_token`, 512),
858
871
  expiresAt: Date.parse(expiresAt),
859
872
  },
860
- request: acquiredRequest(acquired, declaration),
861
873
  };
862
874
  }
863
875
 
package/src/inbound.js CHANGED
@@ -134,13 +134,9 @@ export async function reconcileInbound(directory, session) {
134
134
  }
135
135
 
136
136
  export async function localRequest(directory, run, request) {
137
- const turn = request?.conversation?.turn ?? (request?.kind === "conversation" ? request.turn : null);
138
- if (turn === null || turn === undefined) return request;
137
+ if (request.kind !== "conversation") return request;
139
138
  const result = structuredClone(request);
140
- const localTurn = result.conversation?.turn ?? result.turn;
141
- if (!Array.isArray(localTurn.messages)) throw new KitRefusal("kit_run_input_unreadable");
142
- for (const message of localTurn.messages) {
143
- if (!Array.isArray(message.files)) throw new KitRefusal("kit_run_input_unreadable");
139
+ for (const message of result.turn.messages) {
144
140
  for (const file of message.files) {
145
141
  const parts = typeof file.path === "string" ? file.path.split("/") : [];
146
142
  if (parts.length !== 4 || parts[0] !== ROOT || parts[1] !== run || !UUID.test(parts[1]) || !UUID.test(parts[2]) || !segment(parts[3])) {
@@ -161,15 +157,3 @@ export async function localRequest(directory, run, request) {
161
157
  }
162
158
  return result;
163
159
  }
164
-
165
- export async function localReply(directory, run, frame, reply) {
166
- if (frame.method !== "tools/call" || frame.params?.name !== "session_get_request" || reply.error || reply.result?.isError) return reply;
167
- const result = structuredClone(reply);
168
- for (const item of result.result?.content ?? []) {
169
- if (item.type !== "text") continue;
170
- let input;
171
- try { input = JSON.parse(item.text); } catch { throw new KitRefusal("kit_run_input_unreadable"); }
172
- item.text = JSON.stringify(await localRequest(directory, run, input));
173
- }
174
- return result;
175
- }
package/src/launcher.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { commandHouseInstructions, commandHouseRequest } from "./adapter-mcp.js";
2
+ import { commandHouseInstructions } from "./adapter-mcp.js";
3
3
  import {
4
4
  compiledContract,
5
5
  initializeCommandAdapter,
@@ -191,13 +191,12 @@ async function runTurn(channel, start, bridge, turn) {
191
191
  loading = false;
192
192
  }
193
193
  await configureSession(acp, contract, providerSessionId, start.adapter);
194
- const request = start.request === null ? await commandHouseRequest(bridge) : start.request;
195
194
  turn.adapterSession = true;
196
195
  turn.cancel = () => acp.cancel({ sessionId: providerSessionId });
197
196
  const prompted = await acp.request("session/prompt", {
198
197
  sessionId: providerSessionId,
199
198
  prompt: [
200
- { type: "text", text: JSON.stringify(request) },
199
+ { type: "text", text: JSON.stringify(start.request) },
201
200
  { type: "text", text: instructions },
202
201
  ...workingCopyContext(start.working_copies),
203
202
  ],
@@ -1,6 +1,7 @@
1
1
  import { isAbsolute } from "node:path";
2
- import { parse, stringify } from "yaml";
2
+ import { stringify } from "yaml";
3
3
  import { AGENT_OPERATION_META, ATTACHMENT_FILE_META } from "./contract.js";
4
+ import { answerText, answerYaml } from "./mcp.js";
4
5
  import { derivedOperationId } from "./operation.js";
5
6
  import { localAttachment, sendLocalConversationFile, uploadLocalAttachment } from "./original.js";
6
7
  import { KitRefusal } from "./refusal.js";
@@ -12,18 +13,9 @@ class HouseRefused extends Error {
12
13
  }
13
14
  }
14
15
 
15
- function parsed(text) {
16
- try {
17
- return parse(text);
18
- } catch {
19
- return null;
20
- }
21
- }
22
-
23
16
  function answerOf(frame) {
24
17
  if (frame.error !== undefined || frame.result?.isError === true) throw new HouseRefused(frame);
25
- const text = (frame.result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
26
- const answer = parsed(text);
18
+ const answer = answerYaml(answerText(frame.result));
27
19
  if (answer === null || typeof answer !== "object") throw new KitRefusal("kit_house_answer_unreadable");
28
20
  return answer;
29
21
  }
package/src/mcp.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { parse } from "yaml";
2
3
  import { bridgePeer } from "./peer.js";
3
4
  import { KitRefusal } from "./refusal.js";
4
5
 
@@ -69,3 +70,15 @@ export async function bridgeCall(bridge, request, signal = undefined) {
69
70
  await peer.end();
70
71
  }
71
72
  }
73
+
74
+ export function answerText(result) {
75
+ return (result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
76
+ }
77
+
78
+ export function answerYaml(text) {
79
+ try {
80
+ return parse(text);
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
@@ -100,13 +100,12 @@ function recordedMap(authority) {
100
100
  return new Map(authority.files.map((file) => [file.path, file]));
101
101
  }
102
102
 
103
- function structuredOrReplace(root, relative, recorded, content) {
103
+ export function structuredOrReplace(root, relative, recorded, content) {
104
104
  const path = housePath(root, relative);
105
+ const whole = [{ op: "replace", path, base: recorded.revision, content }];
105
106
  const before = parse(recorded.content, relative);
106
107
  const after = parse(content, relative);
107
- if (!before.ok || !after.ok) {
108
- return [{ op: "replace", path, base: recorded.revision, content }];
109
- }
108
+ if (!before.ok || !after.ok) return whole;
110
109
  const ops = [];
111
110
  if (before.document.preamble !== after.document.preamble) {
112
111
  ops.push({
@@ -124,9 +123,7 @@ function structuredOrReplace(root, relative, recorded, content) {
124
123
  if (JSON.stringify(before.document.fields[name]) === JSON.stringify(after.document.fields[name])) {
125
124
  continue;
126
125
  }
127
- if (after.document.fields[name] === undefined) {
128
- return [{ op: "replace", path, base: recorded.revision, content }];
129
- }
126
+ if (after.document.fields[name] === undefined) return whole;
130
127
  const change = { op: "set_field", path, field: name, value: after.document.fields[name] };
131
128
  if (before.document.fields[name] !== undefined) change.base = before.document.fields[name];
132
129
  ops.push(change);
@@ -139,12 +136,11 @@ function structuredOrReplace(root, relative, recorded, content) {
139
136
  const prior = before.document.sections[name];
140
137
  const next = after.document.sections[name];
141
138
  if (prior === next) continue;
142
- if (prior === undefined || next === undefined) {
143
- return [{ op: "replace", path, base: recorded.revision, content }];
144
- }
139
+ if (prior === undefined || next === undefined) return whole;
145
140
  ops.push({ op: "replace_section", path, section: name, base: prior, content: next });
146
141
  }
147
- return ops.length === 0 ? [{ op: "replace", path, base: recorded.revision, content }] : ops;
142
+ if (ops.length === 0) return whole;
143
+ return applyChanges({ [relative]: recorded.content }, ops, root)[relative] === content ? ops : whole;
148
144
  }
149
145
 
150
146
  export function deriveChanges(state, authority, localFiles) {
package/src/sync.js CHANGED
@@ -41,7 +41,7 @@ function sessionDependencies(options, dependencies) {
41
41
  };
42
42
  }
43
43
 
44
- async function withSession(options, dependencies, operation) {
44
+ export async function withSession(options, dependencies, operation) {
45
45
  const session = await openSession(options, sessionDependencies(options, dependencies));
46
46
  return maintainControl(session, (signal) => operation(session, signal), options.signal);
47
47
  }
package/src/worker.js CHANGED
@@ -10,15 +10,16 @@ import {
10
10
  } from "./agents.js";
11
11
  import { readBindings, removeBinding, writeBinding } from "./binding.js";
12
12
  import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
13
- import { openBridge } from "./bridge.js";
13
+ import { bridgedFrame, openBridge } from "./bridge.js";
14
14
  import { assistantBlocks } from "./blocks.js";
15
15
  import { retryConversationFiles } from "./conversation-originals.js";
16
16
  import { openAssistantDrafts } from "./draft.js";
17
- import { localReply, localRequest, reconcileInbound } from "./inbound.js";
17
+ import { localRequest, reconcileInbound } from "./inbound.js";
18
18
  import { localFiles } from "./local-files.js";
19
+ import { answerText, answerYaml } from "./mcp.js";
19
20
  import { inheritedEnvironment } from "./command.js";
20
21
  import { watchKitWork } from "./kit-work-stream.js";
21
- import { applyWorkingCopies, workingCopyPaths } from "./working-copy.js";
22
+ import { applyWorkingCopies, ownerCaller, pushWorkingCopy, workingCopyPaths } from "./working-copy.js";
22
23
  import { readConfiguration } from "./configuration.js";
23
24
  import { launchBase, readyLaunchDirectory } from "./directory.js";
24
25
  import {
@@ -32,8 +33,9 @@ import {
32
33
  WORKER_HEARTBEAT_MS,
33
34
  WORKER_INHERITED_ENVIRONMENT,
34
35
  WORKER_SUPERVISION_POLL_MS,
36
+ WORKING_COPY_PUSH_METHOD,
35
37
  } from "./contract.js";
36
- import { EpochReportRequired, houseFailed, KitRefusal } from "./refusal.js";
38
+ import { EpochReportRequired, formatCommandError, houseFailed, KitRefusal } from "./refusal.js";
37
39
  import {
38
40
  acceptAssistantText,
39
41
  acquireCommands,
@@ -52,6 +54,7 @@ import {
52
54
  observeSessionStart,
53
55
  observeSessionStop,
54
56
  openSession,
57
+ readRunRequest,
55
58
  replaceSessionAuthority,
56
59
  reportContinuation,
57
60
  reportInterruptOutcome,
@@ -269,6 +272,52 @@ function endOnEpochReport(ended, act) {
269
272
  };
270
273
  }
271
274
 
275
+ function refusedWith(text) {
276
+ const conflicts = answerYaml(text.slice(text.indexOf("\n") + 1))?.conflicts;
277
+ const current = Array.isArray(conflicts)
278
+ ? conflicts.map(({ path, state, revision }) =>
279
+ state === "absent" ? { path, revision: null, content: null } : { path, revision },
280
+ )
281
+ : null;
282
+ return { refused: { code: text.split(":")[0].trim(), text, current } };
283
+ }
284
+
285
+ function acceptedWith(text) {
286
+ const written = answerYaml(text).results.map(({ path, op, revision }) =>
287
+ op === "remove" ? { path, revision: null, content: null } : { path, revision },
288
+ );
289
+ return { refused: null, written };
290
+ }
291
+
292
+ async function sessionPush(context, bound, authority, frame, signal) {
293
+ const edit = async (body, sent) => {
294
+ const answer = await callHouseMcp(
295
+ context.session,
296
+ await sessionAuthority(context.session, bound.session, authority, sent),
297
+ bridgedFrame(
298
+ { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "edit", arguments: { changes: body.changes } } },
299
+ body.operation_id,
300
+ ),
301
+ sent,
302
+ );
303
+ if (answer.error !== undefined) return refusedWith(answer.error.message ?? "kit_house_refused");
304
+ const text = answerText(answer.result);
305
+ return answer.result?.isError === true ? refusedWith(text) : acceptedWith(text);
306
+ };
307
+ let outcome;
308
+ try {
309
+ outcome = await pushWorkingCopy(context, frame.params, { binding: { session: bound.session }, submit: edit }, signal);
310
+ } catch (refusal) {
311
+ if (!(refusal instanceof KitRefusal)) throw refusal;
312
+ outcome = { refused: true, text: refusal.message };
313
+ }
314
+ return {
315
+ jsonrpc: "2.0",
316
+ id: frame.id,
317
+ result: { content: [{ type: "text", text: outcome.text }], ...(outcome.refused ? { isError: true } : {}) },
318
+ };
319
+ }
320
+
272
321
  async function openRunBridge(context, bound, authority, start) {
273
322
  const ended = new AbortController();
274
323
  const handed = (act) => endOnEpochReport(ended, act);
@@ -290,14 +339,15 @@ async function openRunBridge(context, bound, authority, start) {
290
339
  ),
291
340
  secret: (interaction, ceremony) => awaitSecretInput(context.session, interaction, ceremony),
292
341
  settle: handed((interaction, outcome) => settleInteraction(context, interaction, outcome)),
293
- forward: handed((frame, signal) =>
294
- localFiles(async (sent) =>
295
- localReply(context.directory, bound.run, sent, await callHouseMcp(
342
+ forward: handed((frame, signal) => frame.method === WORKING_COPY_PUSH_METHOD
343
+ ? sessionPush(context, bound, authority, frame, signal)
344
+ : localFiles(async (sent) =>
345
+ callHouseMcp(
296
346
  context.session,
297
347
  await sessionAuthority(context.session, bound.session, authority, signal),
298
348
  sent,
299
349
  signal,
300
- )), context.session.dependencies, signal)(frame)),
350
+ ), context.session.dependencies, signal)(frame)),
301
351
  });
302
352
  return { bridge, drafts, ended };
303
353
  }
@@ -467,7 +517,7 @@ async function prepareAssignment(context, assignment, resumed) {
467
517
  reported: null,
468
518
  };
469
519
  const authority = { token: assignment.session.token, expiresAt: assignment.session.expiresAt };
470
- const opened = await openRunBridge(context, bound, authority, {
520
+ const start = {
471
521
  working_directory: assignment.workingDirectory,
472
522
  working_copies: await appliedWorkingCopies(context),
473
523
  execution_authority: context.configuration.executionAuthority,
@@ -479,10 +529,16 @@ async function prepareAssignment(context, assignment, resumed) {
479
529
  effort: route.effort,
480
530
  environment: {},
481
531
  },
482
- request: await localRequest(context.directory, assignment.run, assignment.request),
532
+ request: null,
483
533
  provider_session: assignment.providerSessionId ?? null,
484
- });
485
- return { bound, opened };
534
+ };
535
+ const opened = await openRunBridge(context, bound, authority, start);
536
+ const request = async () => {
537
+ const token = await sessionAuthority(context.session, bound.session, authority);
538
+ start.request = await localRequest(context.directory, bound.run,
539
+ await readRunRequest(context.session, bound.run, token));
540
+ };
541
+ return { bound, opened, request };
486
542
  }
487
543
 
488
544
  async function refuseContinuation(context, command, failure) {
@@ -523,11 +579,12 @@ async function superviseAssignment(context, assignment, resumed = false) {
523
579
  if (undecided(context, refusal)) throw refusal;
524
580
  return refuseAssignment(context, assignment, failure);
525
581
  }
526
- const { bound, opened } = prepared;
582
+ const { bound, opened, request } = prepared;
527
583
  return superviseBound(context, bound, opened, async (spawn) => {
528
584
  if (!resumed) await commitRun(context.session, assignment.run);
529
585
  spawn.committed = true;
530
586
  await writeBinding(context.directory, bound);
587
+ await request();
531
588
  await createSession(context.runtime, {
532
589
  session: bound.tmux,
533
590
  workingDirectory: assignment.workingDirectory,
@@ -549,7 +606,7 @@ async function superviseResumption(context, command) {
549
606
  }
550
607
  return superviseAssignment(context, {
551
608
  ...command, agentId: held.agentId, workingDirectory: held.workingDirectory,
552
- conversation: held.conversation, providerSessionId: held.providerSessionId, request: null,
609
+ conversation: held.conversation, providerSessionId: held.providerSessionId,
553
610
  }, true);
554
611
  }
555
612
 
@@ -952,7 +1009,16 @@ export async function runResident(options, dependencies = {}) {
952
1009
  const wake = residentWake();
953
1010
  const update = residentUpdate(context, wake);
954
1011
  context.update = update;
955
- const served = await serveResident(session, context.directory, options.signal, (serving) => update.request(serving));
1012
+ const served = await serveResident(
1013
+ session,
1014
+ context.directory,
1015
+ options.signal,
1016
+ (serving) => update.request(serving),
1017
+ (request, signal) =>
1018
+ pushWorkingCopy({ directory: context.directory, session }, request, ownerCaller(session), signal).catch(
1019
+ (error) => ({ refused: true, text: formatCommandError(error) }),
1020
+ ),
1021
+ );
956
1022
  const serve = async (signal) => {
957
1023
  context.busy = true;
958
1024
  const watcher = new AbortController();