@runuai/host 0.9.87 → 0.9.89
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/lib/orchestrator.ts +15 -0
- package/package.json +1 -1
- package/src/index.ts +29 -0
- package/src/main.ts +41 -22
- package/src/protocol.ts +22 -0
package/lib/orchestrator.ts
CHANGED
|
@@ -1549,6 +1549,7 @@ export class Orchestrator {
|
|
|
1549
1549
|
// can consume/reorder prompt state while the earlier error still owns it.
|
|
1550
1550
|
let eventTail: Promise<void> = Promise.resolve();
|
|
1551
1551
|
session.onEvent((event) => {
|
|
1552
|
+
this.markChannelActivity(channel.taskId);
|
|
1552
1553
|
// Recycle/teardown deletes the old session before calling close().
|
|
1553
1554
|
// Adapters deliberately emit a final exit event; without this identity
|
|
1554
1555
|
// guard it can delete a replacement or consume its respawn budget.
|
|
@@ -3001,6 +3002,20 @@ export class Orchestrator {
|
|
|
3001
3002
|
* reconciles it to `stopped` (worktree present) and it can be resumed. Since
|
|
3002
3003
|
* `stopped` is not an ACTIVE status, host recovery won't restart it.
|
|
3003
3004
|
*/
|
|
3005
|
+
/** Last observed sign of life per channel: any session event (agent
|
|
3006
|
+
* output, turn boundaries) or an inbound delivery. The idle-stop veto
|
|
3007
|
+
* (ADR-125) reads this — the cloud decides idleness from what it can see,
|
|
3008
|
+
* but only the host can see a turn that is mid-flight RIGHT NOW. */
|
|
3009
|
+
readonly #channelActivity = new Map<string, number>();
|
|
3010
|
+
|
|
3011
|
+
markChannelActivity(taskId: string): void {
|
|
3012
|
+
this.#channelActivity.set(taskId, Date.now());
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
lastChannelActivity(taskId: string): number | null {
|
|
3016
|
+
return this.#channelActivity.get(taskId) ?? null;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3004
3019
|
async stopTask(taskId: string): Promise<{ ok: boolean; error?: string }> {
|
|
3005
3020
|
return this.runTaskLifecycle(taskId, () =>
|
|
3006
3021
|
this.stopTaskWithinLifecycle(taskId),
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1029,6 +1029,28 @@ export const hostCommands: HostCommands = {
|
|
|
1029
1029
|
});
|
|
1030
1030
|
},
|
|
1031
1031
|
|
|
1032
|
+
async taskStop(ctx, taskId, input) {
|
|
1033
|
+
logCommand(ctx, "taskStop", taskId, input.reason);
|
|
1034
|
+
// ADR-125: idle stops are the cloud's judgment from what it can see;
|
|
1035
|
+
// only the host can see a turn that is mid-flight RIGHT NOW. Manual
|
|
1036
|
+
// pause carries no veto — a human's explicit gesture wins.
|
|
1037
|
+
if (input.reason === "idle") {
|
|
1038
|
+
const activeAt = getOrchestrator().lastChannelActivity(taskId);
|
|
1039
|
+
if (activeAt !== null && Date.now() - activeAt < IDLE_STOP_VETO_MS) {
|
|
1040
|
+
return { ok: true, value: { vetoed: "active" as const } };
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const result = await getOrchestrator().stopTask(taskId);
|
|
1044
|
+
if (!result.ok) {
|
|
1045
|
+
return {
|
|
1046
|
+
ok: false,
|
|
1047
|
+
code: HostErrorCode.Internal,
|
|
1048
|
+
message: result.error ?? "stop failed",
|
|
1049
|
+
retryable: true,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
return { ok: true, value: {} };
|
|
1053
|
+
},
|
|
1032
1054
|
async taskStatus(ctx, taskId) {
|
|
1033
1055
|
logCommand(ctx, "taskStatus", taskId);
|
|
1034
1056
|
const runtimeFailure = runtimeUnavailable();
|
|
@@ -1112,6 +1134,9 @@ export const hostCommands: HostCommands = {
|
|
|
1112
1134
|
const runtimeFailure = runtimeUnavailable();
|
|
1113
1135
|
if (runtimeFailure) return runtimeFailure;
|
|
1114
1136
|
try {
|
|
1137
|
+
// An inbound turn is activity: the idle veto must cover the gap between
|
|
1138
|
+
// this delivery landing and the agent's first output event.
|
|
1139
|
+
getOrchestrator().markChannelActivity(taskId);
|
|
1115
1140
|
const result = await getOrchestrator().deliver(taskId, agentId, text);
|
|
1116
1141
|
if (!result.ok) {
|
|
1117
1142
|
return {
|
|
@@ -1532,6 +1557,10 @@ function teardownRuntimeUnavailable(): HostCommandResult<never> | null {
|
|
|
1532
1557
|
};
|
|
1533
1558
|
}
|
|
1534
1559
|
|
|
1560
|
+
/** ADR-125: an idle stop is vetoed when the channel showed any activity
|
|
1561
|
+
* this recently — a delivery landing or any session event. */
|
|
1562
|
+
const IDLE_STOP_VETO_MS = 120_000;
|
|
1563
|
+
|
|
1535
1564
|
const RUNTIME_TASK_ADMISSION_WAIT_MS = 3_000;
|
|
1536
1565
|
|
|
1537
1566
|
/**
|
package/src/main.ts
CHANGED
|
@@ -173,6 +173,7 @@ import {
|
|
|
173
173
|
HOST_MAINTENANCE_READINESS_PROTOCOL_FEATURE,
|
|
174
174
|
HOST_CONFIG_PROTOCOL_FEATURE,
|
|
175
175
|
HOST_REMOTE_LIFECYCLE_PROTOCOL_FEATURE,
|
|
176
|
+
HOST_TASK_STOP_PROTOCOL_FEATURE,
|
|
176
177
|
HOST_ENGINE_ACCOUNTS_PROTOCOL_FEATURE,
|
|
177
178
|
HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
|
|
178
179
|
HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
|
|
@@ -584,6 +585,7 @@ function buildCapabilities(): HostCapabilities {
|
|
|
584
585
|
HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
|
|
585
586
|
HOST_CONFIG_PROTOCOL_FEATURE,
|
|
586
587
|
HOST_REMOTE_LIFECYCLE_PROTOCOL_FEATURE,
|
|
588
|
+
HOST_TASK_STOP_PROTOCOL_FEATURE,
|
|
587
589
|
HOST_ENGINE_ACCOUNTS_PROTOCOL_FEATURE,
|
|
588
590
|
HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
|
|
589
591
|
// The echo adapter cannot execute the in-task CLI. Advertising typed
|
|
@@ -2279,6 +2281,12 @@ async function dispatchCommand(
|
|
|
2279
2281
|
const downInput = expectTaskDownInput(args, 0);
|
|
2280
2282
|
return hostCommands.taskDown(ctx, downInput);
|
|
2281
2283
|
}
|
|
2284
|
+
case "taskStop":
|
|
2285
|
+
return hostCommands.taskStop(
|
|
2286
|
+
ctx,
|
|
2287
|
+
typeof args[0] === "string" ? args[0] : "",
|
|
2288
|
+
(args[1] ?? { reason: "manual" }) as { reason: "idle" | "manual" },
|
|
2289
|
+
);
|
|
2282
2290
|
case "taskStatus":
|
|
2283
2291
|
return hostCommands.taskStatus(ctx, expectString(args, 0));
|
|
2284
2292
|
case "channelEnsure":
|
|
@@ -2606,7 +2614,11 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
|
|
|
2606
2614
|
) {
|
|
2607
2615
|
return { kind: "mcp.op", opId: frame.opId, op: frame.op };
|
|
2608
2616
|
}
|
|
2609
|
-
|
|
2617
|
+
// Names only — never payloads (frames can carry tokens).
|
|
2618
|
+
console.warn(
|
|
2619
|
+
`[host-agent] dropping unknown frame kind=${String(frame.kind)}` +
|
|
2620
|
+
(frame.kind === "command" ? ` command=${String(frame.command)}` : ""),
|
|
2621
|
+
);
|
|
2610
2622
|
return null;
|
|
2611
2623
|
}
|
|
2612
2624
|
|
|
@@ -2853,28 +2865,35 @@ function rawDataToBuffer(data: RawData): Buffer {
|
|
|
2853
2865
|
return Buffer.from(data);
|
|
2854
2866
|
}
|
|
2855
2867
|
|
|
2868
|
+
// Exhaustive BY TYPE: `satisfies Record<keyof HostCommands, true>` makes a
|
|
2869
|
+
// command missing from this map a compile error. The previous hand-written
|
|
2870
|
+
// array silently dropped any frame it lagged behind on — hostUpdate and
|
|
2871
|
+
// hostRestart shipped in 0.9.62 and every remote Update/Restart click since
|
|
2872
|
+
// died here as "dropping unknown frame" / a cloud-side command timeout.
|
|
2873
|
+
const HOST_COMMAND_NAMES = {
|
|
2874
|
+
hostUpdate: true,
|
|
2875
|
+
hostRestart: true,
|
|
2876
|
+
taskUp: true,
|
|
2877
|
+
taskDown: true,
|
|
2878
|
+
taskStatus: true,
|
|
2879
|
+
taskStop: true,
|
|
2880
|
+
channelEnsure: true,
|
|
2881
|
+
channelTeardown: true,
|
|
2882
|
+
channelDeliver: true,
|
|
2883
|
+
channelResolvePermission: true,
|
|
2884
|
+
cloneRepo: true,
|
|
2885
|
+
taskDiff: true,
|
|
2886
|
+
attachmentWrite: true,
|
|
2887
|
+
attachmentRead: true,
|
|
2888
|
+
filesOp: true,
|
|
2889
|
+
channelInterrupt: true,
|
|
2890
|
+
appendTranscript: true,
|
|
2891
|
+
previewEnsure: true,
|
|
2892
|
+
engineAccountRemove: true,
|
|
2893
|
+
} satisfies Record<keyof HostCommands, true>;
|
|
2894
|
+
|
|
2856
2895
|
function isHostCommand(command: string): command is keyof HostCommands {
|
|
2857
|
-
|
|
2858
|
-
// manual allow-list (TS can't derive it from the type), so a missing entry
|
|
2859
|
-
// silently drops the frame and hangs the cloud command.
|
|
2860
|
-
return [
|
|
2861
|
-
"taskUp",
|
|
2862
|
-
"taskDown",
|
|
2863
|
-
"taskStatus",
|
|
2864
|
-
"channelEnsure",
|
|
2865
|
-
"channelTeardown",
|
|
2866
|
-
"channelDeliver",
|
|
2867
|
-
"channelResolvePermission",
|
|
2868
|
-
"cloneRepo",
|
|
2869
|
-
"taskDiff",
|
|
2870
|
-
"attachmentWrite",
|
|
2871
|
-
"attachmentRead",
|
|
2872
|
-
"filesOp",
|
|
2873
|
-
"channelInterrupt",
|
|
2874
|
-
"appendTranscript",
|
|
2875
|
-
"previewEnsure",
|
|
2876
|
-
"engineAccountRemove",
|
|
2877
|
-
].includes(command);
|
|
2896
|
+
return Object.hasOwn(HOST_COMMAND_NAMES, command);
|
|
2878
2897
|
}
|
|
2879
2898
|
|
|
2880
2899
|
function expectString(args: unknown[], index: number): string {
|
package/src/protocol.ts
CHANGED
|
@@ -89,6 +89,10 @@ export const HOST_CONFIG_PROTOCOL_FEATURE = "host-config-v1";
|
|
|
89
89
|
/** Remote update/restart over the bridge (owner-gated cloud routes). */
|
|
90
90
|
export const HOST_REMOTE_LIFECYCLE_PROTOCOL_FEATURE =
|
|
91
91
|
"host-remote-lifecycle-v1";
|
|
92
|
+
/** ADR-125: cloud-initiated resumable task stop (idle auto-stop + manual
|
|
93
|
+
* pause). Idle stops carry reason "idle" and the host may VETO them when a
|
|
94
|
+
* turn is active right now — the veto is an ok result, not an error. */
|
|
95
|
+
export const HOST_TASK_STOP_PROTOCOL_FEATURE = "host-task-stop-v1";
|
|
92
96
|
/** ADR-116: labeled engine accounts on the cloud host page — account list in
|
|
93
97
|
* capabilities, account-targeted login (`label` on engine.login.start), and
|
|
94
98
|
* the engineAccountRemove command. */
|
|
@@ -1111,6 +1115,19 @@ export interface TaskLaunchInput {
|
|
|
1111
1115
|
ownerName?: string;
|
|
1112
1116
|
}
|
|
1113
1117
|
|
|
1118
|
+
/** ADR-125 resumable stop. "idle" = the cloud's sweep decided nobody is
|
|
1119
|
+
* using the task; the host may veto if a turn is live. "manual" = a human
|
|
1120
|
+
* pressed pause; no veto. */
|
|
1121
|
+
export interface TaskStopInput {
|
|
1122
|
+
reason: "idle" | "manual";
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
export interface TaskStopResult {
|
|
1126
|
+
/** Present when the host declined an idle stop because the channel showed
|
|
1127
|
+
* activity within its veto window. The cloud retries on a later sweep. */
|
|
1128
|
+
vetoed?: "active";
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1114
1131
|
export interface TaskDownInput {
|
|
1115
1132
|
taskId: string;
|
|
1116
1133
|
task: TaskCommandTask;
|
|
@@ -1296,6 +1313,11 @@ export interface HostCommands {
|
|
|
1296
1313
|
ctx: CommandContext,
|
|
1297
1314
|
taskId: string,
|
|
1298
1315
|
): Promise<HostCommandResult<TaskStatusResult>>;
|
|
1316
|
+
taskStop(
|
|
1317
|
+
ctx: CommandContext,
|
|
1318
|
+
taskId: string,
|
|
1319
|
+
input: TaskStopInput,
|
|
1320
|
+
): Promise<HostCommandResult<TaskStopResult>>;
|
|
1299
1321
|
channelEnsure(
|
|
1300
1322
|
ctx: CommandContext,
|
|
1301
1323
|
input: ChannelEnsureInput,
|