@ours.network/fleet 1.0.2 → 1.0.4

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.
Files changed (36) hide show
  1. package/README.md +4 -4
  2. package/dist/application/role-command-service.d.ts +29 -1
  3. package/dist/application/role-command-service.js +41 -2
  4. package/dist/application/role-creation-service.d.ts +32 -1
  5. package/dist/application/role-creation-service.js +56 -10
  6. package/dist/application/role-removal-service.d.ts +19 -0
  7. package/dist/application/role-removal-service.js +13 -3
  8. package/dist/application/session-mutations.d.ts +7 -0
  9. package/dist/application/session-mutations.js +8 -0
  10. package/dist/application/task-room-service.d.ts +227 -0
  11. package/dist/application/task-room-service.js +529 -0
  12. package/dist/build-info.json +4 -4
  13. package/dist/cli.js +39 -15
  14. package/dist/docs.d.ts +1 -1
  15. package/dist/docs.js +7 -6
  16. package/dist/harness/codex-app-server-proxy.d.ts +1 -1
  17. package/dist/harness/codex-app-server-proxy.js +23 -9
  18. package/dist/harness/types.d.ts +4 -2
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/owner-channel/channel.d.ts +5 -0
  22. package/dist/owner-channel/channel.js +143 -18
  23. package/dist/owner-channel/commands.d.ts +26 -1
  24. package/dist/owner-channel/commands.js +66 -128
  25. package/dist/rooms-tasks/cli.js +181 -462
  26. package/dist/rooms-tasks/provision.js +71 -8
  27. package/dist/rooms-tasks/types.d.ts +2 -0
  28. package/dist/runner.js +19 -44
  29. package/dist/session/acp.d.ts +8 -3
  30. package/dist/session/acp.js +24 -8
  31. package/dist/session/control.d.ts +10 -1
  32. package/dist/session/control.js +22 -21
  33. package/dist/watchdog/query.d.ts +2 -0
  34. package/dist/watchdog/query.js +7 -3
  35. package/dist/web/server.js +2 -2
  36. package/package.json +1 -1
@@ -4,9 +4,15 @@ import { mkdir, readFile, readdir, rm } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { DEFAULT_OWNER_ATTACHMENT_MIME, canonicalCid, } from '../config.js';
6
6
  import { replaceFileAtomically } from '../atomic-file.js';
7
- import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, interruptOutcome, } from '../session/types.js';
7
+ import { TaskRoomApplicationService } from '../application/task-room-service.js';
8
+ import { RoleLifecycleService } from '../application/role-command-service.js';
9
+ import { RoleRepository } from '../application/role-repository.js';
10
+ import { FleetQueryService } from '../application/fleet-query-service.js';
11
+ import { interruptSession, queueSessionPrompt } from '../application/session-mutations.js';
12
+ import { pickBackend } from '../supervisor/index.js';
13
+ import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/types.js';
8
14
  import { VERSION } from '../version.js';
9
- import { renderMarkdownFailure, renderMarkdownResult, taskStatus, } from '../rooms-tasks/markdown.js';
15
+ import { renderMarkdownFailure, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
10
16
  import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
11
17
  import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
12
18
  import { ownerNotices, } from './notices.js';
@@ -74,10 +80,21 @@ export class OwnerChannel {
74
80
  binder;
75
81
  binderOwnedInternally = false;
76
82
  fleetOps;
83
+ prepareRestart;
77
84
  constructor(options) {
78
85
  this.options = options;
79
86
  this.client = options.client ?? new OursSdkClient(options.env, line => options.log(`[${options.role}] owner channel ${line}`));
80
87
  this.fleetOps = options.fleet ?? fleetCliOps(options.role, options.configPath);
88
+ this.prepareRestart = options.prepareRestart ?? (async (role, mode) => {
89
+ const backend = pickBackend();
90
+ const repository = new RoleRepository({ configPath: options.configPath });
91
+ const query = new FleetQueryService({ repository, supervisor: backend });
92
+ const lifecycle = new RoleLifecycleService({ repository,
93
+ ops: { backend, binPath: process.argv[1], log: options.log },
94
+ configPath: options.configPath,
95
+ status: async (roleId) => (await query.detail(roleId)).status });
96
+ await lifecycle.prepareRestart({ roleIds: [role], mode });
97
+ });
81
98
  this.state = new OwnerChannelState(join(options.stateDir, '.owner-channel-state.json'));
82
99
  this.authorizations = new OwnerAuthorizationState(join(options.stateDir, '.owner-channel-owners.json'), options.config.owners);
83
100
  this.conversations = new OwnerConversationState(join(options.stateDir, '.owner-channel-conversations.json'));
@@ -793,7 +810,7 @@ export class OwnerChannel {
793
810
  outbox = this.outboxDir(originWireId);
794
811
  await mkdir(outbox, { recursive: true, mode: 0o700 });
795
812
  const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
796
- const queued = await this.options.session.queuePrompt(this.ownerAttachmentPrompt(sender, originWireId, requestId, admitted, group.caption), {
813
+ const queued = await queueSessionPrompt(this.options.session, this.ownerAttachmentPrompt(sender, originWireId, requestId, admitted, group.caption), {
797
814
  interrupt: this.options.config.interrupt,
798
815
  ...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
799
816
  origin: { kind: 'owner', requestId,
@@ -918,7 +935,7 @@ export class OwnerChannel {
918
935
  let queued;
919
936
  const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
920
937
  try {
921
- queued = await this.options.session.queuePrompt(this.ownerPrompt(sender, text, wireId), {
938
+ queued = await queueSessionPrompt(this.options.session, this.ownerPrompt(sender, text, wireId), {
922
939
  interrupt: this.options.config.interrupt,
923
940
  ...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
924
941
  origin: { kind: 'owner', requestId, displayText: text },
@@ -977,7 +994,7 @@ export class OwnerChannel {
977
994
  harness: this.options.harness,
978
995
  version: VERSION,
979
996
  snapshot: () => this.options.session.snapshot(),
980
- interrupt: async () => interruptOutcome(await this.options.session.interrupt('owner')),
997
+ interrupt: () => interruptSession(this.options.session, 'owner'),
981
998
  runHarnessCommand: command => this.runHarnessCommand(sender, command, wireId),
982
999
  restart: mode => this.restartSelf(sender, mode, wireId),
983
1000
  comments: () => this.commentsState(),
@@ -990,7 +1007,37 @@ export class OwnerChannel {
990
1007
  },
991
1008
  fleetList: () => this.fleetOps.list(),
992
1009
  closeRoom: roomId => this.closeRoomFromOwner(sender, roomId, wireId),
1010
+ recoverRoom: roomId => this.recoverRoomFromOwner(sender, roomId, wireId),
993
1011
  terminalTask: (taskId, kind, outcome) => this.terminalTaskFromOwner(sender, taskId, kind, outcome, wireId),
1012
+ recoverTask: taskId => this.recoverTaskFromOwner(sender, taskId, wireId),
1013
+ createTask: input => new TaskRoomApplicationService(this.options.configPath).createTask({
1014
+ ...input,
1015
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1016
+ }),
1017
+ startTask: taskId => new TaskRoomApplicationService(this.options.configPath).startTask({
1018
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1019
+ }),
1020
+ listTasks: filter => new TaskRoomApplicationService(this.options.configPath).listTasks(filter),
1021
+ getTask: taskId => new TaskRoomApplicationService(this.options.configPath).getTask(taskId),
1022
+ blockTask: (taskId, reason) => new TaskRoomApplicationService(this.options.configPath).blockTask({
1023
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId, reason,
1024
+ }),
1025
+ unblockTask: taskId => new TaskRoomApplicationService(this.options.configPath).unblockTask({
1026
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1027
+ }),
1028
+ reviewTask: taskId => new TaskRoomApplicationService(this.options.configPath).reviewTask({
1029
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1030
+ }),
1031
+ deleteTask: taskId => new TaskRoomApplicationService(this.options.configPath).deleteTask({
1032
+ actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
1033
+ }),
1034
+ listRoomQueries: filter => new TaskRoomApplicationService(this.options.configPath).listRooms(filter),
1035
+ getRoomQuery: id => new TaskRoomApplicationService(this.options.configPath).getRoomDetail(id),
1036
+ listTemplateQueries: () => new TaskRoomApplicationService(this.options.configPath).listTemplates(),
1037
+ getTemplateQuery: name => new TaskRoomApplicationService(this.options.configPath).getTemplate(name),
1038
+ createRoom: input => new TaskRoomApplicationService(this.options.configPath).createRoom({
1039
+ ...input, actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
1040
+ }),
994
1041
  recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
995
1042
  readWorklogTail: maxChars => this.readWorklogTail(maxChars),
996
1043
  reply: async (replyText) => { await this.send(sender.id, replyText, wireId); },
@@ -1009,7 +1056,7 @@ export class OwnerChannel {
1009
1056
  /** Queue raw slash text to the harness and report the turn's outcome. */
1010
1057
  async runHarnessCommand(sender, command, wireId) {
1011
1058
  const requestId = this.requestId(wireId);
1012
- const queued = await this.options.session.queuePrompt(command, {
1059
+ const queued = await queueSessionPrompt(this.options.session, command, {
1013
1060
  origin: { kind: 'owner', requestId },
1014
1061
  });
1015
1062
  this.inFlight.add(wireId);
@@ -1036,6 +1083,7 @@ export class OwnerChannel {
1036
1083
  */
1037
1084
  async restartSelf(sender, mode, wireId) {
1038
1085
  const command = mode === 'fresh' ? '/force-restart' : '/restart';
1086
+ await this.prepareRestart(this.options.role, mode);
1039
1087
  await this.send(sender.id, ownerNotices.restarting(this.options.role, command, mode), wireId);
1040
1088
  this.state.remember(wireId);
1041
1089
  this.options.log(`[${this.options.role}] owner requested ${command}`);
@@ -1046,8 +1094,9 @@ export class OwnerChannel {
1046
1094
  * before an external worker starts a saga that can retire this process.
1047
1095
  */
1048
1096
  async closeRoomFromOwner(sender, roomId, wireId) {
1049
- const { acceptManagedRoomClose, recordManagedRoomCloseError } = await import('../rooms-tasks/close.js');
1050
- await acceptManagedRoomClose(roomId);
1097
+ const app = new TaskRoomApplicationService(this.options.configPath);
1098
+ const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
1099
+ await app.requestRoomDeletion({ actor, roomId });
1051
1100
  await this.send(sender.id, renderMarkdownFailure({
1052
1101
  kind: 'pending', subject: `/room delete ${roomId} ${roomId}`,
1053
1102
  detail: 'The deletion request was accepted and is still being settled.',
@@ -1058,24 +1107,97 @@ export class OwnerChannel {
1058
1107
  await this.fleetOps.closeRoom(roomId);
1059
1108
  }
1060
1109
  catch (error) {
1061
- await recordManagedRoomCloseError(roomId, error instanceof Error ? error.message : String(error), `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.`);
1110
+ await app.recordRoomSettlementError({ actor, roomId,
1111
+ error: error instanceof Error ? error.message : String(error),
1112
+ recoveryHint: `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.` });
1062
1113
  throw error;
1063
1114
  }
1064
1115
  }
1116
+ async recoverRoomFromOwner(sender, roomId, wireId) {
1117
+ const app = new TaskRoomApplicationService(this.options.configPath);
1118
+ const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
1119
+ const result = await app.recoverRoom({ actor, roomId });
1120
+ if (result.kind === 'deletion_worker_required') {
1121
+ await this.send(sender.id, renderMarkdownFailure({ kind: 'pending',
1122
+ subject: `/room recover ${roomId}`,
1123
+ detail: 'The deletion recovery is still being settled.',
1124
+ action: `Run /room recover ${roomId} if deletion remains pending.` }), wireId);
1125
+ this.state.remember(wireId);
1126
+ try {
1127
+ await this.fleetOps.closeRoom(roomId);
1128
+ }
1129
+ catch (error) {
1130
+ await app.recordRoomSettlementError({ actor, roomId,
1131
+ error: error instanceof Error ? error.message : String(error),
1132
+ recoveryHint: `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.` });
1133
+ throw error;
1134
+ }
1135
+ return;
1136
+ }
1137
+ const r = result.orchestration;
1138
+ await this.send(sender.id, renderMarkdownResult({ icon: '🛟', title: 'Room recovery',
1139
+ fields: [{ label: 'Room', value: result.room.room_id, kind: 'code' },
1140
+ { label: 'Status', value: roomStatus(result.room.state), kind: 'markdown' },
1141
+ ...(r ? [{ label: 'Saga', value: r.saga.phase, kind: 'code' }] : [])],
1142
+ sections: result.issues.length ? [{ heading: 'Next steps', items: result.issues }]
1143
+ : [{ heading: 'Result', items: ['No recovery action is needed.'] }] }), wireId);
1144
+ this.state.remember(wireId);
1145
+ }
1065
1146
  /** Carry a task terminal request through a worker that survives this role. */
1147
+ async recoverTaskFromOwner(sender, taskId, wireId) {
1148
+ const app = new TaskRoomApplicationService(this.options.configPath);
1149
+ const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
1150
+ const begin = await app.beginTaskRecovery({ actor, taskId });
1151
+ if (begin.kind === 'terminal_worker_required') {
1152
+ await this.send(sender.id, renderMarkdownFailure({
1153
+ kind: 'pending', subject: `/task recover ${taskId}`,
1154
+ detail: 'The recovery request was accepted and is still being settled.',
1155
+ action: `Run /task recover ${taskId} if it remains pending.`,
1156
+ }), wireId);
1157
+ this.state.remember(wireId);
1158
+ try {
1159
+ await this.fleetOps.recoverTask(taskId);
1160
+ }
1161
+ catch (error) {
1162
+ await app.recordSettlementError({ actor, taskId,
1163
+ error: error instanceof Error ? error.message : String(error),
1164
+ recoveryHint: `External settle worker failed to start. Retry /task recover ${taskId}.` });
1165
+ throw error;
1166
+ }
1167
+ return;
1168
+ }
1169
+ const { task, room, issues } = begin.result;
1170
+ const hints = issues.map(issue => issue.code === 'waiting_cowork' ? 'Cowork socket unreachable'
1171
+ : issue.code === 'waiting_owner_invite' ? 'Owner invite missing or invalid'
1172
+ : issue.code === 'owner_cid_mismatch' ? 'Owner CID mismatch'
1173
+ : issue.code === 'member_failed' ? `Member failed at step ${issue.stepIndex}`
1174
+ : issue.code === 'resume_failed' ? `Resume failed: ${issue.error}`
1175
+ : issue.code === 'provisioning_resumed' ? 'Provisioning resumed successfully'
1176
+ : issue.code);
1177
+ await this.send(sender.id, renderMarkdownResult({
1178
+ icon: '🛟', title: 'Task recovery',
1179
+ fields: [{ label: 'Task', value: task.task_id, kind: 'code' },
1180
+ { label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
1181
+ ...(room ? [{ label: 'Room', value: room.room_id, kind: 'code' },
1182
+ { label: 'Room status', value: roomStatus(room.state), kind: 'markdown' },
1183
+ { label: 'Saga', value: room.saga.phase, kind: 'code' }] : [])],
1184
+ sections: hints.length ? [{ heading: 'Next steps', items: hints }]
1185
+ : [{ heading: 'Result', items: ['No automated recovery action is available.'] }],
1186
+ }), wireId);
1187
+ this.state.remember(wireId);
1188
+ }
1066
1189
  async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
1067
- const { getTask } = await import('../rooms-tasks/task-state.js');
1068
- const { acceptTaskTerminalIntent, recordTaskTerminalIntentError, } = await import('../rooms-tasks/terminal.js');
1069
- const task = getTask(taskId);
1070
- const accepted = await acceptTaskTerminalIntent({
1071
- taskId, kind, roomId: task.room_id, outcome,
1072
- });
1073
- if (accepted.terminal_intent?.status === 'settled') {
1190
+ const app = new TaskRoomApplicationService(this.options.configPath);
1191
+ const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
1192
+ const plan = kind === 'done'
1193
+ ? await app.completeTask({ actor, taskId, outcome })
1194
+ : await app.cancelTask({ actor, taskId });
1195
+ if (!plan.settlementRequired) {
1074
1196
  await this.send(sender.id, renderMarkdownResult({
1075
1197
  icon: '📋', title: 'Task terminal action complete',
1076
1198
  fields: [
1077
1199
  { label: 'ID', value: taskId, kind: 'code' },
1078
- { label: 'Status', value: taskStatus(accepted.state), kind: 'markdown' },
1200
+ { label: 'Status', value: taskStatus(plan.task.state), kind: 'markdown' },
1079
1201
  ],
1080
1202
  }), wireId);
1081
1203
  this.state.remember(wireId);
@@ -1091,7 +1213,10 @@ export class OwnerChannel {
1091
1213
  await this.fleetOps.settleTask(taskId);
1092
1214
  }
1093
1215
  catch (error) {
1094
- await recordTaskTerminalIntentError(taskId, error instanceof Error ? error.message : String(error), `External settle worker failed to start. Retry the identical task command or run task recover ${taskId}.`);
1216
+ await app.recordSettlementError({
1217
+ actor, taskId, error: error instanceof Error ? error.message : String(error),
1218
+ recoveryHint: `External settle worker failed to start. Retry the identical task command or run task recover ${taskId}.`,
1219
+ });
1095
1220
  throw error;
1096
1221
  }
1097
1222
  }
@@ -1,6 +1,8 @@
1
1
  import type { InterruptOutcome, SessionEvent, SessionSnapshot } from '../session/types.js';
2
- import type { TaskOutcome, TaskTerminalIntent } from '../rooms-tasks/types.js';
2
+ import type { RoomOrchestrationRecord, TaskOutcome, TaskRecord, TaskTerminalIntent } from '../rooms-tasks/types.js';
3
3
  import { type OwnerCommentsState } from './notices.js';
4
+ import type { CreateRoomRequest, CreateTaskRequest, TaskRoomApplicationService } from '../application/task-room-service.js';
5
+ import type { TaskState } from '../rooms-tasks/types.js';
4
6
  /**
5
7
  * Fleet-level effects a deterministic owner command may trigger. Production
6
8
  * uses the detached CLI (`fleetCliOps`); tests inject fakes so no command can
@@ -15,6 +17,7 @@ export interface OwnerFleetOps {
15
17
  closeRoom(roomId: string): Promise<void>;
16
18
  /** Resume a task terminal intent outside the caller role's supervisor lifecycle. */
17
19
  settleTask(taskId: string): Promise<void>;
20
+ recoverTask(taskId: string): Promise<void>;
18
21
  }
19
22
  /**
20
23
  * The narrow capability surface a command executor sees. Everything here is
@@ -47,8 +50,30 @@ export interface OwnerCommandContext {
47
50
  fleetList(): Promise<string>;
48
51
  /** Persist acceptance, acknowledge it, then launch the external close worker. */
49
52
  closeRoom(roomId: string): Promise<void>;
53
+ recoverRoom(roomId: string): Promise<void>;
50
54
  /** Persist terminal intent, acknowledge it, then launch the external settle worker. */
51
55
  terminalTask(taskId: string, kind: TaskTerminalIntent['kind'], outcome?: TaskOutcome): Promise<void>;
56
+ recoverTask(taskId: string): Promise<void>;
57
+ createTask(input: Omit<CreateTaskRequest, 'actor'>): Promise<TaskRecord>;
58
+ startTask(taskId: string): Promise<TaskRecord>;
59
+ listTasks(filter?: {
60
+ state?: TaskState | TaskState[];
61
+ }): TaskRecord[];
62
+ getTask(taskId: string): {
63
+ task: TaskRecord;
64
+ orchestration: RoomOrchestrationRecord | undefined;
65
+ };
66
+ blockTask(taskId: string, reason: string): TaskRecord;
67
+ unblockTask(taskId: string): TaskRecord;
68
+ reviewTask(taskId: string): TaskRecord;
69
+ deleteTask(taskId: string): boolean;
70
+ listRoomQueries(filter?: {
71
+ state?: 'active' | 'provisioning';
72
+ }): ReturnType<TaskRoomApplicationService['listRooms']>;
73
+ getRoomQuery(id: string): ReturnType<TaskRoomApplicationService['getRoomDetail']>;
74
+ listTemplateQueries(): ReturnType<TaskRoomApplicationService['listTemplates']>;
75
+ getTemplateQuery(name: string): ReturnType<TaskRoomApplicationService['getTemplate']>;
76
+ createRoom(input: Omit<CreateRoomRequest, 'actor'>): Promise<RoomOrchestrationRecord>;
52
77
  recentEvents(limit: number): SessionEvent[];
53
78
  readWorklogTail(maxChars: number): Promise<string | undefined>;
54
79
  reply(text: string): Promise<void>;