mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 17:20:53 +08:00
feat: support inbound files across all channels
This commit is contained in:
parent
29bcb67a64
commit
0a26d7df92
45 changed files with 2651 additions and 343 deletions
|
|
@ -3,6 +3,10 @@ import { randomUUID } from 'node:crypto';
|
|||
import { isAbsolute } from 'node:path';
|
||||
|
||||
import { adoptRegisteredWorkspaceSession } from './harness-session-binding.mjs';
|
||||
import {
|
||||
appendInboundFilesToPrompt,
|
||||
InboundFileError,
|
||||
} from './inbound-file.mjs';
|
||||
import { outboundArtifactRegistry } from './semantic/artifact.mjs';
|
||||
|
||||
// Every channel plugin runs in the same Host process. Sharing ownership by
|
||||
|
|
@ -466,6 +470,7 @@ export class HarnessClient {
|
|||
#commandExecutor;
|
||||
#controlExecutor;
|
||||
#sessionMaintenanceExecutor;
|
||||
#fileIngressExecutor;
|
||||
#managedProcess = null;
|
||||
#interactionRegistry;
|
||||
#interactionOwnerships;
|
||||
|
|
@ -486,6 +491,7 @@ export class HarnessClient {
|
|||
commandExecutor,
|
||||
controlExecutor,
|
||||
sessionMaintenanceExecutor,
|
||||
fileIngressExecutor,
|
||||
}) {
|
||||
if (typeof createWebSocket !== 'function') {
|
||||
throw new TypeError('createWebSocket must be a function');
|
||||
|
|
@ -509,6 +515,9 @@ export class HarnessClient {
|
|||
&& typeof sessionMaintenanceExecutor !== 'function') {
|
||||
throw new TypeError('sessionMaintenanceExecutor must be a function');
|
||||
}
|
||||
if (fileIngressExecutor !== undefined && typeof fileIngressExecutor !== 'function') {
|
||||
throw new TypeError('fileIngressExecutor must be a function');
|
||||
}
|
||||
this.#baseUrl = new URL(baseUrl);
|
||||
this.#workspace = workspace;
|
||||
// Keep an omitted preset absent so session.create resolves the Host's current default.
|
||||
|
|
@ -523,6 +532,7 @@ export class HarnessClient {
|
|||
this.#commandExecutor = commandExecutor;
|
||||
this.#controlExecutor = controlExecutor;
|
||||
this.#sessionMaintenanceExecutor = sessionMaintenanceExecutor;
|
||||
this.#fileIngressExecutor = fileIngressExecutor;
|
||||
this.#interactionRegistry = interactionRegistry(this.#baseUrl.origin);
|
||||
this.#interactionOwnerships = this.#interactionRegistry.ownerships;
|
||||
this.#interactionClaims = this.#interactionRegistry.claims;
|
||||
|
|
@ -1034,6 +1044,7 @@ export class HarnessClient {
|
|||
? options.onInteractionResolved
|
||||
: undefined;
|
||||
const control = normalizeControl(options.control);
|
||||
const inboundFiles = Array.isArray(options.files) ? options.files.filter(Boolean) : [];
|
||||
await this.ensureRunning({ signal });
|
||||
const before = await this.rpc(
|
||||
'session.history',
|
||||
|
|
@ -1075,6 +1086,9 @@ export class HarnessClient {
|
|||
let interactionTask = null;
|
||||
let artifactsDelivered = false;
|
||||
let deliveredArtifactCount = 0;
|
||||
let stagedInboundFiles = null;
|
||||
let promptAccepted = false;
|
||||
let turnFinished = false;
|
||||
|
||||
const deliverArtifacts = async () => {
|
||||
if (!onArtifact || artifactsDelivered || tracker.turn === null) {
|
||||
|
|
@ -1103,6 +1117,30 @@ export class HarnessClient {
|
|||
const closeArtifactConsumer = outboundArtifactRegistry.openConsumer(sessionId, promptRpcId);
|
||||
|
||||
try {
|
||||
if (inboundFiles.length > 0) {
|
||||
if (!this.#fileIngressExecutor) {
|
||||
throw new InboundFileError(
|
||||
'inbound-file-ingress-unavailable',
|
||||
'Harness file ingress is unavailable in this Host process.',
|
||||
);
|
||||
}
|
||||
const sessionList = await this.rpc(
|
||||
'session.list',
|
||||
{},
|
||||
30_000,
|
||||
{ signal },
|
||||
);
|
||||
const sessionWorkspace = sessionList?.items?.find(
|
||||
(item) => item?.sessionId === sessionId,
|
||||
)?.cwd;
|
||||
stagedInboundFiles = await this.#fileIngressExecutor({
|
||||
sessionId,
|
||||
workspace: sessionWorkspace,
|
||||
files: inboundFiles,
|
||||
signal,
|
||||
});
|
||||
prompt = appendInboundFilesToPrompt(prompt, stagedInboundFiles);
|
||||
}
|
||||
if (interactionSignal) {
|
||||
let markOpen;
|
||||
const opened = new Promise((resolve) => { markOpen = resolve; });
|
||||
|
|
@ -1134,6 +1172,7 @@ export class HarnessClient {
|
|||
content,
|
||||
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
}, 30_000, { rpcId: promptRpcId, signal });
|
||||
promptAccepted = true;
|
||||
|
||||
try {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
|
@ -1159,6 +1198,7 @@ export class HarnessClient {
|
|||
}
|
||||
}
|
||||
if (!tracker.finished) continue;
|
||||
turnFinished = true;
|
||||
// An accepted /stop revokes attachment delivery even when Harness
|
||||
// preserved a useful partial text answer for the existing UX.
|
||||
const artifactCount = ownership?.stopRequested
|
||||
|
|
@ -1185,6 +1225,11 @@ export class HarnessClient {
|
|||
throw turnStoppedError();
|
||||
}
|
||||
} finally {
|
||||
if (stagedInboundFiles && (!promptAccepted || turnFinished)) {
|
||||
await stagedInboundFiles.cleanup().catch((error) => {
|
||||
console.warn(`[${this.#logPrefix}] unable to clean inbound files:`, error.message);
|
||||
});
|
||||
}
|
||||
closeArtifactConsumer();
|
||||
if (ownership) {
|
||||
this.#unregisterControlOwnership(ownership);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue