@linxin666/dsh-client-ui-task-board 0.4.6 → 0.4.7
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.i18n.yaml +2 -2
- package/README.md +14 -6
- package/README.zh.md +14 -6
- package/lib/client.js +1502 -820
- package/lib/client.js.map +1 -1
- package/lib/index.js +674 -75
- package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +49 -0
- package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
- package/lib/types/client/board/model-options.d.ts +67 -0
- package/lib/types/client/board/preset-label.d.ts +21 -0
- package/lib/types/client/board/preset-roster.d.ts +45 -0
- package/lib/types/client/board/team-run-promotion.d.ts +26 -0
- package/lib/types/client/locales.d.ts +16 -0
- package/lib/types/core/card-defaults.d.ts +78 -0
- package/lib/types/core/controller.d.ts +32 -7
- package/lib/types/core/launch-width.d.ts +31 -0
- package/lib/types/core/reasoning-effort.d.ts +57 -0
- package/lib/types/core/subtask.d.ts +30 -8
- package/lib/types/core/tasks.d.ts +19 -0
- package/lib/types/core/use-cases/task-create.d.ts +10 -5
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host/launch-admission.d.ts +72 -0
- package/lib/types/host-ledger.d.ts +9 -4
- package/lib/types/host-runner.d.ts +79 -4
- package/lib/types/host-service.d.ts +85 -2
- package/lib/types/index.d.ts +87 -0
- package/lib/types/protocol.d.ts +13 -0
- package/package.json +28 -28
- package/src/client/CardDefaultPresetField.tsx +101 -0
- package/src/client/TaskBoardSettingsCard.tsx +214 -16
- package/src/client/board/LinkSubtaskModal.tsx +8 -0
- package/src/client/board/NewTaskModal.tsx +86 -40
- package/src/client/board/ReasoningEffortField.tsx +50 -0
- package/src/client/board/TaskDetail.tsx +42 -6
- package/src/client/board/model-options.ts +178 -0
- package/src/client/board/preset-label.ts +32 -0
- package/src/client/board/preset-roster.ts +58 -0
- package/src/client/board/team-run-promotion.ts +39 -0
- package/src/client/index.ts +98 -94
- package/src/client/locales.ts +34 -2
- package/src/client/settings-entry-form.ts +0 -1
- package/src/core/card-defaults.ts +132 -0
- package/src/core/controller.ts +44 -9
- package/src/core/launch-width.ts +38 -0
- package/src/core/reasoning-effort.ts +92 -0
- package/src/core/store.ts +5 -0
- package/src/core/subtask.ts +48 -13
- package/src/core/tasks.ts +20 -0
- package/src/core/use-cases/task-create.ts +27 -16
- package/src/core/use-cases/task-update.ts +6 -1
- package/src/host/agent-tools.ts +21 -8
- package/src/host/launch-admission.ts +140 -0
- package/src/host-ledger.ts +18 -6
- package/src/host-runner.ts +184 -18
- package/src/host-service.ts +247 -23
- package/src/index.ts +149 -2
- package/src/protocol.ts +28 -4
package/src/host-service.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { TypertGateway } from '@deepseek-ai/dsh-api-gateway'
|
|
2
|
+
import { DEFAULT_RUN_LAUNCH_WIDTH, normalizeRunLaunchWidth } from './core/launch-width.ts'
|
|
2
3
|
import { DEFAULT_SESSION_POLL_SECONDS, normalizeSessionPollSeconds, sessionPollMs } from './core/poll-cadence.ts'
|
|
3
4
|
import { reusableSessionId } from './core/session-reuse.ts'
|
|
5
|
+
import { LaunchAdmissionQueue, type QueuedLaunch } from './host/launch-admission.ts'
|
|
4
6
|
import { HostTaskLedger, type OpenedRun, type OpenExecutionReference } from './host-ledger.ts'
|
|
5
|
-
import { HostExecutionRunner, SessionLaunchError, promptText, type SessionCommandDispatcher, type SessionSummary, type TaskBoardWorkspaceRegistry } from './host-runner.ts'
|
|
7
|
+
import { HostExecutionRunner, SessionLaunchError, promptText, type GatewayPeer, type SessionCommandDispatcher, type SessionSummary, type TaskBoardWorkspaceRegistry } from './host-runner.ts'
|
|
6
8
|
import { teammateName } from './core/subtask.ts'
|
|
7
9
|
import { workspaceOwningSession } from './core/workspace-target.ts'
|
|
8
10
|
import { PowerInhibitor } from './power-inhibitor.ts'
|
|
9
11
|
import { TASK_BOARD_SCHEMA_VERSION, type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardSnapshot } from './protocol.ts'
|
|
12
|
+
import type { CardDefaults } from './core/card-defaults.ts'
|
|
10
13
|
import { TaskBoardExtensionRegistry } from './host/extension-registry.ts'
|
|
11
14
|
import type { TaskBoardExtension } from './core/extension.ts'
|
|
12
|
-
import type
|
|
15
|
+
import { isTaskPermission, normalizeTargetId, type ExecutionOutcome, type ScheduleMode, type TaskRecord, type TaskStatus } from './core/tasks.ts'
|
|
13
16
|
import { passedAttempt, resolveContract, verificationNeverInvoked, verificationOnlyInvalid, verificationRequired, withAcceptanceDetailCleared, type ExecutionVerification, type ModelCatalogView, type VerificationContract, type VerificationSettings } from './core/verification.ts'
|
|
14
17
|
import type { TaskPermission } from './core/handover.ts'
|
|
15
18
|
|
|
@@ -65,6 +68,35 @@ const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
|
65
68
|
*/
|
|
66
69
|
const UNREADABLE_SETTLE_POLLS = 24
|
|
67
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Why a member of a run never started: the Host stopped while its launch was
|
|
73
|
+
* still waiting for an admission slot. Recorded rather than left open, so the
|
|
74
|
+
* card leaves the running column instead of waiting for the next Host start's
|
|
75
|
+
* interrupted-start recovery.
|
|
76
|
+
*/
|
|
77
|
+
const ABANDONED_LAUNCH_REASON = 'host stopped before this execution started'
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The acceptance contract one run group freezes, read once for the whole group.
|
|
81
|
+
*
|
|
82
|
+
* The board freezes an execution's acceptance contract BEFORE its session is
|
|
83
|
+
* prompted. Admission delays the launch, so reading these per execution would
|
|
84
|
+
* let a settings edit made while later members wait reach some members of one
|
|
85
|
+
* run and not others. Reading them once per run group — at the group's first
|
|
86
|
+
* admission, which is its root's — keeps the whole group on one contract and
|
|
87
|
+
* still freezes it before any member's session is prompted.
|
|
88
|
+
*/
|
|
89
|
+
interface FrozenRunContract {
|
|
90
|
+
settings: VerificationSettings
|
|
91
|
+
/** The GLOBAL native-/goal switch, resolved exactly once for the run group. */
|
|
92
|
+
globalGoalEnabled: boolean
|
|
93
|
+
/** The host model catalog the group's contracts resolve their route from. */
|
|
94
|
+
catalog: ModelCatalogView | undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Reads (or returns the already-read) contract of one run group. */
|
|
98
|
+
type RunContractFreezer = () => Promise<FrozenRunContract>
|
|
99
|
+
|
|
68
100
|
/**
|
|
69
101
|
* Why an enforced goal execution settled failed while its acceptance DID run:
|
|
70
102
|
* attempts were recorded and none of them passed. The judge answered about the
|
|
@@ -105,10 +137,18 @@ export interface ScheduledRunContext {
|
|
|
105
137
|
}
|
|
106
138
|
|
|
107
139
|
/**
|
|
108
|
-
* Actions that can
|
|
109
|
-
* an unrelated card edit leaves the pending fire
|
|
140
|
+
* Actions that can change the instant the schedule timer is armed at. Only
|
|
141
|
+
* these re-arm the native timer; an unrelated card edit leaves the pending fire
|
|
142
|
+
* untouched.
|
|
143
|
+
*
|
|
144
|
+
* The set is the write surface of a rule itself rather than a guess at the UI
|
|
145
|
+
* flows that reach it: `create` and `import` land a card that already carries
|
|
146
|
+
* an enabled rule (the new-task dialog and the one-time browser import),
|
|
147
|
+
* `set-schedule` edits one, and `delete`/`archive` drop one. `update` is
|
|
148
|
+
* deliberately absent — the update patch cannot carry a schedule — and neither
|
|
149
|
+
* can the run, move, lineage, permission, verification or label actions.
|
|
110
150
|
*/
|
|
111
|
-
const SCHEDULE_WRITE_ACTIONS: ReadonlySet<TaskBoardAction['kind']> = new Set(['set-schedule', 'delete', 'archive'])
|
|
151
|
+
const SCHEDULE_WRITE_ACTIONS: ReadonlySet<TaskBoardAction['kind']> = new Set(['create', 'import', 'set-schedule', 'delete', 'archive'])
|
|
112
152
|
|
|
113
153
|
/**
|
|
114
154
|
* The native timer face the Host arms through. The cordis `timer` service
|
|
@@ -134,6 +174,29 @@ const PROCESS_TIMERS: HostTimerFace = {
|
|
|
134
174
|
},
|
|
135
175
|
}
|
|
136
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The new-card defaults a snapshot publishes, or undefined when this
|
|
179
|
+
* deployment configures none. A target the board configures nothing for is
|
|
180
|
+
* omitted instead of published blank, so the browser can tell "no default
|
|
181
|
+
* model" from "a blank one" (only a hand-edited settings document can express
|
|
182
|
+
* the latter).
|
|
183
|
+
*/
|
|
184
|
+
function cardDefaultsOf(defaults: CardDefaults): TaskBoardSnapshot['cardDefaults'] {
|
|
185
|
+
const mode = normalizeTargetId(defaults.mode)
|
|
186
|
+
const permission = normalizeTargetId(defaults.permission)
|
|
187
|
+
const model = normalizeTargetId(defaults.model)
|
|
188
|
+
const reasoningEffort = normalizeTargetId(defaults.reasoningEffort)
|
|
189
|
+
const published: NonNullable<TaskBoardSnapshot['cardDefaults']> = {
|
|
190
|
+
...(mode === undefined ? {} : { mode }),
|
|
191
|
+
// The snapshot publishes a permission the board's own vocabulary knows; an
|
|
192
|
+
// out-of-domain value is ignored here exactly as the create path ignores it.
|
|
193
|
+
...(isTaskPermission(permission) ? { permission } : {}),
|
|
194
|
+
...(model === undefined ? {} : { model }),
|
|
195
|
+
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
196
|
+
}
|
|
197
|
+
return Object.keys(published).length === 0 ? undefined : published
|
|
198
|
+
}
|
|
199
|
+
|
|
137
200
|
export class TaskBoardHostService {
|
|
138
201
|
readonly ledger: HostTaskLedger
|
|
139
202
|
readonly runner: HostExecutionRunner
|
|
@@ -154,6 +217,12 @@ export class TaskBoardHostService {
|
|
|
154
217
|
private disposed = false
|
|
155
218
|
private pollInFlight = false
|
|
156
219
|
private active = true
|
|
220
|
+
/**
|
|
221
|
+
* Admission control for the session-creating half of a run: one board action
|
|
222
|
+
* opens a whole subtree, and this is what keeps the Host from asking DSH for
|
|
223
|
+
* every member's session at once (see {@link LaunchAdmissionQueue}).
|
|
224
|
+
*/
|
|
225
|
+
private readonly launches: LaunchAdmissionQueue
|
|
157
226
|
/**
|
|
158
227
|
* Consecutive unreadable-history polls per open execution (see
|
|
159
228
|
* {@link noteUnreadableInspection}). Cleared as soon as an inspection
|
|
@@ -167,6 +236,14 @@ export class TaskBoardHostService {
|
|
|
167
236
|
private readonly timers: HostTimerFace
|
|
168
237
|
/** Live reader of the global native-/goal switch; frozen per execution. */
|
|
169
238
|
private readonly goalRunEnabled: () => boolean
|
|
239
|
+
/** Live reader of the deployment's new-card defaults (published and applied per create). */
|
|
240
|
+
private readonly cardDefaults: () => CardDefaults
|
|
241
|
+
/**
|
|
242
|
+
* Live reader of the run-launch admission width: how many launches may be in
|
|
243
|
+
* flight at once. Read at every admission, so a settings edit bounds the next
|
|
244
|
+
* launch without remounting the row. Absent means the default.
|
|
245
|
+
*/
|
|
246
|
+
private readonly runLaunchWidth: () => number
|
|
170
247
|
private lastPowerJson = ''
|
|
171
248
|
private readonly now: () => number
|
|
172
249
|
/** The deployment's workspaces, consulted to resolve a card that pins none. */
|
|
@@ -184,12 +261,26 @@ export class TaskBoardHostService {
|
|
|
184
261
|
now?: () => number
|
|
185
262
|
commandDispatcher?: SessionCommandDispatcher
|
|
186
263
|
workspaceRegistry?: TaskBoardWorkspaceRegistry
|
|
264
|
+
/**
|
|
265
|
+
* The owner peer every gateway request the runner makes speaks for.
|
|
266
|
+
* Production passes the peer bound to the plugin fiber (see
|
|
267
|
+
* {@link createOwnerPeer}); absent is fine for tests and bare mounts,
|
|
268
|
+
* where the runner binds one to a fresh root context.
|
|
269
|
+
*/
|
|
270
|
+
ownerPeer?: GatewayPeer
|
|
187
271
|
/**
|
|
188
272
|
* Permission baseline of the confirmation gate: a fixed value, or a live
|
|
189
273
|
* resolver the board re-reads so a Host Settings change needs no remount.
|
|
190
274
|
*/
|
|
191
275
|
sessionDefaultPermission?: TaskPermission | (() => TaskPermission)
|
|
192
276
|
maxSubtaskDepth?: number
|
|
277
|
+
/**
|
|
278
|
+
* Live reader of the deployment's new-card defaults (volatile config reads).
|
|
279
|
+
* The board publishes them for the create dialog and re-reads them for every
|
|
280
|
+
* create that opts in, so a settings edit reaches the next created card
|
|
281
|
+
* without remounting the row. Absent means the deployment configures none.
|
|
282
|
+
*/
|
|
283
|
+
cardDefaults?: () => CardDefaults
|
|
193
284
|
team?: TaskBoardTeamDispatcher
|
|
194
285
|
timers?: HostTimerFace
|
|
195
286
|
/**
|
|
@@ -208,18 +299,29 @@ export class TaskBoardHostService {
|
|
|
208
299
|
verificationSettings?: () => VerificationSettings
|
|
209
300
|
/** The host model catalog the acceptance contract resolves its route from. */
|
|
210
301
|
verificationCatalog?: () => Promise<ModelCatalogView | undefined>
|
|
302
|
+
/**
|
|
303
|
+
* Live admission width for run launches: the number of launches (one
|
|
304
|
+
* session creation and its prompt each) that may be in flight at once. Read
|
|
305
|
+
* per admission, so editing the setting bounds the next launch of a run that
|
|
306
|
+
* is still queueing. Absent means the default width.
|
|
307
|
+
*/
|
|
308
|
+
runLaunchWidth?: () => number
|
|
211
309
|
} = {}) {
|
|
212
310
|
this.ledger = options.ledger ?? new HostTaskLedger(undefined, undefined, {
|
|
213
311
|
sessionDefaultPermission: options.sessionDefaultPermission,
|
|
214
312
|
maxSubtaskDepth: options.maxSubtaskDepth,
|
|
313
|
+
cardDefaults: options.cardDefaults,
|
|
215
314
|
})
|
|
216
|
-
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry)
|
|
315
|
+
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry, undefined, options.ownerPeer)
|
|
217
316
|
this.workspaceRegistry = options.workspaceRegistry
|
|
218
317
|
this.team = options.team
|
|
219
318
|
this.timers = options.timers ?? PROCESS_TIMERS
|
|
220
319
|
this.power = options.power ?? new PowerInhibitor()
|
|
221
320
|
this.now = options.now ?? Date.now
|
|
222
321
|
this.goalRunEnabled = options.goalRunEnabled ?? (() => false)
|
|
322
|
+
this.cardDefaults = options.cardDefaults ?? (() => ({}))
|
|
323
|
+
this.runLaunchWidth = options.runLaunchWidth ?? (() => DEFAULT_RUN_LAUNCH_WIDTH)
|
|
324
|
+
this.launches = new LaunchAdmissionQueue(() => normalizeRunLaunchWidth(this.runLaunchWidth()))
|
|
223
325
|
this.verificationSettings = options.verificationSettings ?? (() => ({ enabled: false, model: '', reasoningEffort: '' }))
|
|
224
326
|
this.verificationCatalog = options.verificationCatalog ?? (async () => undefined)
|
|
225
327
|
this.extensions = new TaskBoardExtensionRegistry({
|
|
@@ -310,6 +412,7 @@ export class TaskBoardHostService {
|
|
|
310
412
|
snapshot(): TaskBoardSnapshot {
|
|
311
413
|
const state = this.ledger.state()
|
|
312
414
|
const published = this.extensions.published()
|
|
415
|
+
const cardDefaults = cardDefaultsOf(this.cardDefaults())
|
|
313
416
|
return {
|
|
314
417
|
schemaVersion: TASK_BOARD_SCHEMA_VERSION,
|
|
315
418
|
revision: state.revision,
|
|
@@ -323,6 +426,10 @@ export class TaskBoardHostService {
|
|
|
323
426
|
// service freezes its own read at launch: this mirror is for the FORM
|
|
324
427
|
// (enable/disable + explanation), never for a run already in flight.
|
|
325
428
|
goalRunEnabled: this.goalRunEnabled(),
|
|
429
|
+
// The new-card defaults are published live for the same reason the switch
|
|
430
|
+
// is: the create dialog prefills from them, and each creating session
|
|
431
|
+
// re-reads them for the create it submits.
|
|
432
|
+
...(cardDefaults === undefined ? {} : { cardDefaults }),
|
|
326
433
|
...(Object.keys(published).length === 0 ? {} : { extensions: published }),
|
|
327
434
|
}
|
|
328
435
|
}
|
|
@@ -427,9 +534,11 @@ export class TaskBoardHostService {
|
|
|
427
534
|
}
|
|
428
535
|
const result = this.ledger.applyRequest(requestId, this.withInheritedWorkspace(action, initiator), initiator)
|
|
429
536
|
if (result.runs !== undefined) this.dispatchRuns(result.runs)
|
|
430
|
-
// A committed
|
|
431
|
-
//
|
|
432
|
-
//
|
|
537
|
+
// A committed write that can move a rule moves the nearest trigger;
|
|
538
|
+
// re-arm from the ledger so a created or edited schedule fires at its own
|
|
539
|
+
// instant instead of waiting out whatever target was armed before it. The
|
|
540
|
+
// re-arm is derived from the ledger, so a write that moved nothing leaves
|
|
541
|
+
// the pending fire exactly as it was.
|
|
433
542
|
if (SCHEDULE_WRITE_ACTIONS.has(action.kind)) this.refreshSchedule()
|
|
434
543
|
this.emitStatusChanges(before, initiator)
|
|
435
544
|
if (action.kind === 'delete') this.extensions.emitTaskDeleted({ taskId: action.taskId })
|
|
@@ -474,6 +583,14 @@ export class TaskBoardHostService {
|
|
|
474
583
|
|
|
475
584
|
dispose(): void {
|
|
476
585
|
this.disposed = true
|
|
586
|
+
// Launches still waiting for a slot can never start now: settle them so
|
|
587
|
+
// their cards leave the running column here, instead of waiting for the
|
|
588
|
+
// next Host start's interrupted-start recovery. Launches already in flight
|
|
589
|
+
// are left to finish — their session may already exist.
|
|
590
|
+
const abandoned = this.launches.drain()
|
|
591
|
+
if (abandoned.length > 0) {
|
|
592
|
+
safeConsoleError(`[dsh-task-board] ${abandoned.length} run member(s) were still waiting for a launch slot when the Host stopped; each is settled as cancelled`)
|
|
593
|
+
}
|
|
477
594
|
this.extensions.dispose()
|
|
478
595
|
this.clearScheduleTimer()
|
|
479
596
|
this.clearPollBackoff()
|
|
@@ -484,7 +601,7 @@ export class TaskBoardHostService {
|
|
|
484
601
|
this.listeners.clear()
|
|
485
602
|
}
|
|
486
603
|
|
|
487
|
-
private async launch(opened: OpenedRun, others: readonly OpenedRun[] = [], schedule?: ScheduledRunContext): Promise<void> {
|
|
604
|
+
private async launch(opened: OpenedRun, others: readonly OpenedRun[] = [], schedule?: ScheduledRunContext, freezeContract?: RunContractFreezer): Promise<void> {
|
|
488
605
|
/** The session this launch attached to the execution, once it exists. */
|
|
489
606
|
let attached: string | undefined
|
|
490
607
|
try {
|
|
@@ -512,11 +629,16 @@ export class TaskBoardHostService {
|
|
|
512
629
|
// retrofit the rule that will judge it. A card that opted out resolves the
|
|
513
630
|
// same contract from an OFF switch: the board-wide switch decides the
|
|
514
631
|
// default, the card decides this execution, and both are read here.
|
|
515
|
-
|
|
516
|
-
// The
|
|
517
|
-
//
|
|
632
|
+
//
|
|
633
|
+
// The read is the run GROUP's when the queue handed one down: admission
|
|
634
|
+
// delays a member's launch, so a per-execution read would let a settings
|
|
635
|
+
// edit made while later members wait reach some members of one run and not
|
|
636
|
+
// others. One group, one contract — and still frozen before the group's
|
|
637
|
+
// first session is prompted.
|
|
638
|
+
const { settings, globalGoalEnabled, catalog } = await (freezeContract ?? (() => this.freezeRunContract()))()
|
|
639
|
+
// The GLOBAL native-/goal switch is read ONCE per run group and carried by
|
|
640
|
+
// value for the whole execution: a settings edit after this point can only
|
|
518
641
|
// affect executions that start later.
|
|
519
|
-
const globalGoalEnabled = this.goalRunEnabled() === true
|
|
520
642
|
// The card must ASK for the goal run: the option is off by default, so an
|
|
521
643
|
// absent value is a plain turn exactly like an explicit false.
|
|
522
644
|
const goalEnabled = globalGoalEnabled && opened.task.goalRun === true
|
|
@@ -525,7 +647,7 @@ export class TaskBoardHostService {
|
|
|
525
647
|
const skippedBy: ExecutionVerification['applicability'] = opened.task.skipVerification === true ? 'skipped' : 'disabled'
|
|
526
648
|
const contract = resolveContract(
|
|
527
649
|
{ ...settings, enabled: settings.enabled && opened.task.skipVerification !== true },
|
|
528
|
-
|
|
650
|
+
catalog,
|
|
529
651
|
)
|
|
530
652
|
// An execution that will not become a goal run can never fire the
|
|
531
653
|
// completion gate, so it is marked with the reason that actually applied:
|
|
@@ -549,6 +671,8 @@ export class TaskBoardHostService {
|
|
|
549
671
|
// The frozen global switch travels with the launch, so the runner's
|
|
550
672
|
// arming decision is exactly the one made above.
|
|
551
673
|
goalEnabled,
|
|
674
|
+
// The same catalog snapshot the acceptance contract was frozen from.
|
|
675
|
+
catalog,
|
|
552
676
|
// Bind the session to the execution before the prompt is queued, so the
|
|
553
677
|
// completion gate can never observe the agent without its binding.
|
|
554
678
|
onSession: (id) => {
|
|
@@ -573,16 +697,60 @@ export class TaskBoardHostService {
|
|
|
573
697
|
// failed launch can never silently spend the user's run budget. A manual
|
|
574
698
|
// run, and a launch that DID reach a session, are never refunded.
|
|
575
699
|
if (schedule !== undefined && attached === undefined && launchedSession === undefined) {
|
|
576
|
-
this.
|
|
700
|
+
this.refundUnstartedSchedule(opened.task.id)
|
|
577
701
|
}
|
|
578
702
|
this.settleAndNotify(opened.task.id, opened.execution.id, 'failed', error instanceof Error ? error.message : String(error))
|
|
579
703
|
}
|
|
580
704
|
}
|
|
581
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Read the acceptance contract one run group freezes, in the order the board
|
|
708
|
+
* has always read it: the acceptance settings, then the global native-/goal
|
|
709
|
+
* switch, then the model catalog that both resolves the contract's route and
|
|
710
|
+
* decides the reasoning effort the session may be given.
|
|
711
|
+
*/
|
|
712
|
+
private async freezeRunContract(): Promise<FrozenRunContract> {
|
|
713
|
+
const settings = this.verificationSettings()
|
|
714
|
+
const globalGoalEnabled = this.goalRunEnabled() === true
|
|
715
|
+
const catalog = await this.verificationCatalog()
|
|
716
|
+
return { settings, globalGoalEnabled, catalog }
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* A {@link RunContractFreezer} that reads once and hands the SAME value to
|
|
721
|
+
* every later caller of one run group. A read that fails is not cached: the
|
|
722
|
+
* next member reads again, exactly as a per-execution read would have.
|
|
723
|
+
*/
|
|
724
|
+
private contractFreezerForRunGroup(): RunContractFreezer {
|
|
725
|
+
let pending: Promise<FrozenRunContract> | undefined
|
|
726
|
+
return () => {
|
|
727
|
+
pending ??= this.freezeRunContract().catch((error: unknown) => {
|
|
728
|
+
pending = undefined
|
|
729
|
+
throw error
|
|
730
|
+
})
|
|
731
|
+
return pending
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Give back the occurrence a launch consumed, for a launch that never reached
|
|
737
|
+
* a session: a failed launch, or one the Host abandoned while it was still
|
|
738
|
+
* waiting for an admission slot. A manual run, and a launch that DID reach a
|
|
739
|
+
* session, are never refunded.
|
|
740
|
+
*/
|
|
741
|
+
private refundUnstartedSchedule(taskId: string): void {
|
|
742
|
+
this.ledger.refundScheduledOccurrence(taskId, this.now())
|
|
743
|
+
// The refund runs after the fire already armed the next target, and a
|
|
744
|
+
// refunded occurrence can restore one the fire had just cleared (a rule
|
|
745
|
+
// that spent its last run). Re-arm so the restored occurrence is served
|
|
746
|
+
// at its own instant rather than lost until some unrelated write.
|
|
747
|
+
this.refreshSchedule()
|
|
748
|
+
}
|
|
749
|
+
|
|
582
750
|
private scheduleTeammate(opened: OpenedRun, leadSessionId: string): void {
|
|
583
|
-
|
|
751
|
+
this.enqueueLaunch(opened, () => this.spawnTeammate(opened, leadSessionId).catch(error => {
|
|
584
752
|
safeConsoleError('[dsh-task-board] teammate spawn settlement failed', error)
|
|
585
|
-
})
|
|
753
|
+
}))
|
|
586
754
|
}
|
|
587
755
|
|
|
588
756
|
/**
|
|
@@ -966,14 +1134,22 @@ export class TaskBoardHostService {
|
|
|
966
1134
|
* member then launches on its own so one refused participant cannot hold the
|
|
967
1135
|
* others back. A team run's members are spawned inside the root's Lead
|
|
968
1136
|
* session instead, once that session exists.
|
|
1137
|
+
*
|
|
1138
|
+
* Every launch of one run set goes through the admission queue, which admits
|
|
1139
|
+
* at most `runLaunchWidth` of them at a time — the whole subtree still opens,
|
|
1140
|
+
* members just enter the running column in batches. Order inside the group is
|
|
1141
|
+
* the order they are enqueued here, so the root's session always exists before
|
|
1142
|
+
* any member's, and one member's launch settling (or failing) is what admits
|
|
1143
|
+
* the next. All members share the one contract this group freezes.
|
|
969
1144
|
*/
|
|
970
1145
|
private dispatchRuns(runs: readonly OpenedRun[], schedule?: ScheduledRunContext): void {
|
|
971
1146
|
if (runs.length === 0) return
|
|
972
1147
|
const root = runs.find(run => run.dispatch !== 'teammate') ?? runs[0]
|
|
973
1148
|
const others = runs.filter(run => run !== root)
|
|
974
|
-
this.
|
|
1149
|
+
const freezeContract = this.contractFreezerForRunGroup()
|
|
1150
|
+
this.scheduleLaunch(root, others, schedule, freezeContract)
|
|
975
1151
|
for (const run of others) {
|
|
976
|
-
if (run.dispatch !== 'teammate') this.scheduleLaunch(run, [], schedule)
|
|
1152
|
+
if (run.dispatch !== 'teammate') this.scheduleLaunch(run, [], schedule, freezeContract)
|
|
977
1153
|
}
|
|
978
1154
|
}
|
|
979
1155
|
|
|
@@ -987,10 +1163,58 @@ export class TaskBoardHostService {
|
|
|
987
1163
|
this.ledger.setVerification(taskId, executionId, { ...verification, applicability })
|
|
988
1164
|
}
|
|
989
1165
|
|
|
990
|
-
private scheduleLaunch(opened: OpenedRun, others: readonly OpenedRun[] = [], schedule?: ScheduledRunContext): void {
|
|
991
|
-
|
|
1166
|
+
private scheduleLaunch(opened: OpenedRun, others: readonly OpenedRun[] = [], schedule?: ScheduledRunContext, freezeContract?: RunContractFreezer): void {
|
|
1167
|
+
this.enqueueLaunch(opened, () => this.launch(opened, others, schedule, freezeContract).catch(error => {
|
|
992
1168
|
safeConsoleError('[dsh-task-board] execution launch settlement failed', error)
|
|
993
|
-
})
|
|
1169
|
+
}), schedule)
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Hand one launch to the admission queue, or abandon it on the spot when the
|
|
1174
|
+
* Host is already stopping: a queue entry created after {@link dispose} would
|
|
1175
|
+
* otherwise never be drained, and its card would sit in the running column
|
|
1176
|
+
* until the next Host start recovered it.
|
|
1177
|
+
*/
|
|
1178
|
+
private enqueueLaunch(opened: OpenedRun, start: () => Promise<void>, schedule?: ScheduledRunContext): void {
|
|
1179
|
+
const launch: QueuedLaunch = {
|
|
1180
|
+
groupId: opened.execution.runGroupId ?? opened.task.id,
|
|
1181
|
+
start: async () => {
|
|
1182
|
+
// Waiting for a slot is time the execution can be settled in: a manual
|
|
1183
|
+
// settle, or a team run folded onto its Lead's verdict, can close a
|
|
1184
|
+
// member's execution before it ever started. Starting it then would
|
|
1185
|
+
// create a session no execution is watching, so a launch whose
|
|
1186
|
+
// execution is already closed is dropped instead.
|
|
1187
|
+
if (!this.isExecutionOpen(opened)) return
|
|
1188
|
+
await start()
|
|
1189
|
+
},
|
|
1190
|
+
abandon: () => this.abandonQueuedRun(opened, schedule),
|
|
1191
|
+
}
|
|
1192
|
+
if (this.disposed) {
|
|
1193
|
+
launch.abandon()
|
|
1194
|
+
return
|
|
1195
|
+
}
|
|
1196
|
+
this.launches.enqueue(launch)
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/** Whether one opened execution is still open in the ledger. */
|
|
1200
|
+
private isExecutionOpen(opened: OpenedRun): boolean {
|
|
1201
|
+
const execution = this.ledger.getTask(opened.task.id)?.executions.find(entry => entry.id === opened.execution.id)
|
|
1202
|
+
return execution !== undefined && execution.endedAt === undefined
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Take one launch that never started out of the running column.
|
|
1207
|
+
*
|
|
1208
|
+
* Settled as CANCELLED, not failed: nothing about the run's work went wrong —
|
|
1209
|
+
* the Host stopped before the launch got a slot, which is exactly what the
|
|
1210
|
+
* next start's interrupted-start recovery records for an execution that never
|
|
1211
|
+
* reached a session. A scheduled occurrence that never reached a session is
|
|
1212
|
+
* refunded on the same terms as any other pre-session failure, so a run the
|
|
1213
|
+
* Host never started cannot silently spend the user's run budget.
|
|
1214
|
+
*/
|
|
1215
|
+
private abandonQueuedRun(opened: OpenedRun, schedule?: ScheduledRunContext): void {
|
|
1216
|
+
if (schedule !== undefined) this.refundUnstartedSchedule(opened.task.id)
|
|
1217
|
+
this.settleAndNotify(opened.task.id, opened.execution.id, 'cancelled', ABANDONED_LAUNCH_REASON)
|
|
994
1218
|
}
|
|
995
1219
|
|
|
996
1220
|
private schedulePoll(): void {
|
package/src/index.ts
CHANGED
|
@@ -18,10 +18,12 @@ import type {} from '@deepseek-ai/dsh-tools'
|
|
|
18
18
|
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
|
19
19
|
import type { LlmRuntime } from '@deepseek-ai/dsh-llm'
|
|
20
20
|
import { TaskBoardHostService, type HostTimerFace, type TaskBoardTeamDispatcher } from './host-service.ts'
|
|
21
|
+
import { createOwnerPeer } from './host-runner.ts'
|
|
21
22
|
import { parseTaskDraft, TaskParseError } from './host-ai.ts'
|
|
22
23
|
import { TASK_PERMISSIONS, isTaskPermission, type TaskPermission } from './core/tasks.ts'
|
|
23
24
|
import { DEFAULT_SUBTASK_DEPTH, SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from './core/subtask.ts'
|
|
24
25
|
import { DEFAULT_SESSION_POLL_SECONDS, SESSION_POLL_MAX_SECONDS, SESSION_POLL_MIN_SECONDS } from './core/poll-cadence.ts'
|
|
26
|
+
import { DEFAULT_RUN_LAUNCH_WIDTH, RUN_LAUNCH_WIDTH_MAX, RUN_LAUNCH_WIDTH_MIN } from './core/launch-width.ts'
|
|
25
27
|
import {
|
|
26
28
|
DEFAULT_VERIFICATION_BUDGET_SECONDS,
|
|
27
29
|
DEFAULT_VERIFICATION_CALL_TIMEOUT_SECONDS,
|
|
@@ -33,6 +35,7 @@ import {
|
|
|
33
35
|
verificationCallTimeoutMs,
|
|
34
36
|
} from './core/verification-budget.ts'
|
|
35
37
|
import { DEFAULT_SESSION_PERMISSION } from './core/handover.ts'
|
|
38
|
+
import type { CardDefaults } from './core/card-defaults.ts'
|
|
36
39
|
import { buildTaskBoardTools, TASK_BOARD_TOOL_NAMES } from './host/agent-tools.ts'
|
|
37
40
|
import { PLUGIN_TOOL_SECTION_ORDERS, visibleToolText } from './tool-surface.ts'
|
|
38
41
|
import { makeTaskBoardRoutes } from './host-routes.ts'
|
|
@@ -106,6 +109,17 @@ export interface Config {
|
|
|
106
109
|
* at all, so this cadence only bounds how quickly a running card settles.
|
|
107
110
|
*/
|
|
108
111
|
sessionPollSeconds?: Volatile<number>
|
|
112
|
+
/**
|
|
113
|
+
* How many run launches this board may have in flight at once (1..16,
|
|
114
|
+
* default 3). One run opens its whole subtree, so without a bound the Host
|
|
115
|
+
* asks DSH for every member's session in the same tick; this is the mechanism
|
|
116
|
+
* that enforces the repository's measured guidance for heavy implementation
|
|
117
|
+
* subagents. Members wait their turn instead of being dropped: the subtree
|
|
118
|
+
* still opens completely, they only enter the running column in batches. Read
|
|
119
|
+
* at each admission, so an edit bounds the next launch of a run that is still
|
|
120
|
+
* queueing.
|
|
121
|
+
*/
|
|
122
|
+
runLaunchWidth?: Volatile<number>
|
|
109
123
|
/**
|
|
110
124
|
* Continuable-subagent provider the Agent Teams service uses to compose a
|
|
111
125
|
* teammate. Matches the Agent Teams tool plugin's `freshProvider` default;
|
|
@@ -128,6 +142,33 @@ export interface Config {
|
|
|
128
142
|
* becomes a goal run.
|
|
129
143
|
*/
|
|
130
144
|
goalRunEnabled?: Volatile<boolean>
|
|
145
|
+
/**
|
|
146
|
+
* Agent preset a NEW card starts from (volatile, default blank = not set).
|
|
147
|
+
* A prefill, never a pin: the create dialog opens with it selected, the
|
|
148
|
+
* caller may override it, and the card stores what it was created with. It
|
|
149
|
+
* reaches the create path only when that caller opts in with
|
|
150
|
+
* `applyCardDefaults`, so the extension provider's own create and the legacy
|
|
151
|
+
* import are untouched.
|
|
152
|
+
*/
|
|
153
|
+
cardDefaultMode?: Volatile<string>
|
|
154
|
+
/**
|
|
155
|
+
* Permission a NEW card starts from (volatile, default blank = not set), one
|
|
156
|
+
* of the three presets. A prefill like the rest: a value above
|
|
157
|
+
* `sessionDefaultPermission` still has to be confirmed by a human before the
|
|
158
|
+
* card may run, because the pre-execution gate is what decides that.
|
|
159
|
+
*/
|
|
160
|
+
cardDefaultPermission?: Volatile<TaskPermission | ''>
|
|
161
|
+
/**
|
|
162
|
+
* Model a NEW card starts from (volatile, default blank = not set).
|
|
163
|
+
*/
|
|
164
|
+
cardDefaultModel?: Volatile<string>
|
|
165
|
+
/**
|
|
166
|
+
* Reasoning level a NEW card starts from (volatile, default blank = not set).
|
|
167
|
+
* Bound to `cardDefaultModel` exactly as a card's own level is bound to its
|
|
168
|
+
* own model: it applies only while the card's final model IS that default
|
|
169
|
+
* model, and a blank `cardDefaultModel` makes it inert.
|
|
170
|
+
*/
|
|
171
|
+
cardDefaultReasoningEffort?: Volatile<string>
|
|
131
172
|
/**
|
|
132
173
|
* Goal acceptance for executions this board starts (default ON). When on,
|
|
133
174
|
* update_goal(action: complete) inside a task execution is refused until an
|
|
@@ -205,10 +246,20 @@ export interface ConfigInput {
|
|
|
205
246
|
maxSubtaskDepth?: number
|
|
206
247
|
/** Roster-poll cadence in seconds while the board has work to reconcile. */
|
|
207
248
|
sessionPollSeconds?: number
|
|
249
|
+
/** Concurrent run launches allowed at once, 1..16. */
|
|
250
|
+
runLaunchWidth?: number
|
|
208
251
|
/** Continuable-subagent provider the Agent Teams service composes a teammate from. */
|
|
209
252
|
teamProvider?: string
|
|
210
253
|
/** Global native /goal switch; default off, frozen per execution. */
|
|
211
254
|
goalRunEnabled?: boolean
|
|
255
|
+
/** Agent preset a new card starts from; blank = not set. */
|
|
256
|
+
cardDefaultMode?: string
|
|
257
|
+
/** Permission a new card starts from; blank = not set. */
|
|
258
|
+
cardDefaultPermission?: TaskPermission | ''
|
|
259
|
+
/** Model a new card starts from; blank = not set. */
|
|
260
|
+
cardDefaultModel?: string
|
|
261
|
+
/** Reasoning level a new card starts from; blank = not set. */
|
|
262
|
+
cardDefaultReasoningEffort?: string
|
|
212
263
|
/** Goal acceptance switch. */
|
|
213
264
|
goalVerification?: boolean
|
|
214
265
|
/** Judge model route for goal acceptance; blank inherits the host default. */
|
|
@@ -230,8 +281,13 @@ export const Config: z<ConfigInput, Config> = z.object({
|
|
|
230
281
|
sessionDefaultPermission: z.union(TASK_PERMISSIONS),
|
|
231
282
|
maxSubtaskDepth: z.number().min(SUBTASK_DEPTH_MIN).max(SUBTASK_DEPTH_MAX).default(DEFAULT_SUBTASK_DEPTH).volatile(),
|
|
232
283
|
sessionPollSeconds: z.number().min(SESSION_POLL_MIN_SECONDS).max(SESSION_POLL_MAX_SECONDS).default(DEFAULT_SESSION_POLL_SECONDS).volatile(),
|
|
284
|
+
runLaunchWidth: z.number().min(RUN_LAUNCH_WIDTH_MIN).max(RUN_LAUNCH_WIDTH_MAX).default(DEFAULT_RUN_LAUNCH_WIDTH).volatile(),
|
|
233
285
|
teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER),
|
|
234
286
|
goalRunEnabled: z.boolean().default(false).volatile(),
|
|
287
|
+
cardDefaultMode: z.string().default('').volatile(),
|
|
288
|
+
cardDefaultPermission: z.union([...TASK_PERMISSIONS, '']).default('').volatile(),
|
|
289
|
+
cardDefaultModel: z.string().default('').volatile(),
|
|
290
|
+
cardDefaultReasoningEffort: z.string().default('').volatile(),
|
|
235
291
|
goalVerification: z.boolean().default(true).volatile(),
|
|
236
292
|
goalVerificationModel: z.string().default('').volatile(),
|
|
237
293
|
goalVerificationReasoningEffort: z.string().default('').volatile(),
|
|
@@ -465,6 +521,70 @@ export function resolveLlmRuntime(ctx: Context): LlmRuntime | undefined {
|
|
|
465
521
|
}
|
|
466
522
|
}
|
|
467
523
|
|
|
524
|
+
/**
|
|
525
|
+
* The subset of `sessionController` the board uses to wake a cold persisted
|
|
526
|
+
* session before running a slash command on it (issue #1861).
|
|
527
|
+
*/
|
|
528
|
+
interface SessionControllerFace {
|
|
529
|
+
resolveAgent(
|
|
530
|
+
sessionId: Parameters<Context['agents']['get']>[0],
|
|
531
|
+
): Promise<
|
|
532
|
+
| { readonly agent: Parameters<Context['commands']['execute']>[0] }
|
|
533
|
+
| { readonly error: { readonly message?: string } }
|
|
534
|
+
>
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Resolve the optional `sessionController` service. Like the tool registry and
|
|
539
|
+
* the llm runtime, it is read dynamically so capture-only test contexts that do
|
|
540
|
+
* not mount the full session controller still activate the board.
|
|
541
|
+
* @param ctx - the plugin context.
|
|
542
|
+
* @returns the session controller face, or undefined when unavailable.
|
|
543
|
+
*/
|
|
544
|
+
export function resolveSessionController(ctx: Context): SessionControllerFace | undefined {
|
|
545
|
+
try {
|
|
546
|
+
const controller = (ctx as { get?: (name: string) => unknown }).get?.('sessionController') as SessionControllerFace | undefined
|
|
547
|
+
return controller !== undefined && typeof controller.resolveAgent === 'function' ? controller : undefined
|
|
548
|
+
} catch {
|
|
549
|
+
return undefined
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Resolve the live Agent for an execution session, resuming a cold persisted
|
|
555
|
+
* session through `sessionController.resolveAgent` when it is not yet in
|
|
556
|
+
* `ctx.agents` (issue #1861).
|
|
557
|
+
*
|
|
558
|
+
* A fresh run (`session/create`) or a session the browser is currently
|
|
559
|
+
* following is already in `ctx.agents`, so the fast path returns immediately.
|
|
560
|
+
* A scheduled or manual reuse of a cold session — one that `session/list`
|
|
561
|
+
* reports as idle from disk without activating its Agent — is not in
|
|
562
|
+
* `ctx.agents` when `pinAndPrompt` runs `/permission` ahead of
|
|
563
|
+
* `session/selectModel` and `session/prompt`; delegating to
|
|
564
|
+
* `sessionController.resolveAgent` resumes it with its recorded preset and
|
|
565
|
+
* model-selection state, matching what those gateway endpoints do next.
|
|
566
|
+
* @param ctx - the plugin context.
|
|
567
|
+
* @param sessionId - the target execution session id.
|
|
568
|
+
* @returns the live Agent for `sessionId`.
|
|
569
|
+
*/
|
|
570
|
+
export async function resolveExecutionAgent(
|
|
571
|
+
ctx: Context,
|
|
572
|
+
sessionId: Parameters<Context['agents']['get']>[0],
|
|
573
|
+
): Promise<Parameters<Context['commands']['execute']>[0]> {
|
|
574
|
+
const live = ctx.agents.get(sessionId)
|
|
575
|
+
if (live !== undefined) return live
|
|
576
|
+
const controller = resolveSessionController(ctx)
|
|
577
|
+
if (controller !== undefined) {
|
|
578
|
+
const resolved = await controller.resolveAgent(sessionId)
|
|
579
|
+
if ('agent' in resolved && resolved.agent !== undefined) return resolved.agent
|
|
580
|
+
const detail = 'error' in resolved && typeof resolved.error?.message === 'string' && resolved.error.message !== ''
|
|
581
|
+
? `: ${resolved.error.message}`
|
|
582
|
+
: ''
|
|
583
|
+
throw new Error(`execution session ${sessionId} is not available${detail}`)
|
|
584
|
+
}
|
|
585
|
+
throw new Error(`execution session ${sessionId} is not available`)
|
|
586
|
+
}
|
|
587
|
+
|
|
468
588
|
/**
|
|
469
589
|
* Activate the board's host half: ledger, routes, cron scheduler, power
|
|
470
590
|
* inhibitor, and the model-facing announcement.
|
|
@@ -491,6 +611,12 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
491
611
|
const maxSubtaskDepth = (): number => readConfigField(config?.maxSubtaskDepth, DEFAULT_SUBTASK_DEPTH)
|
|
492
612
|
/** Current roster-poll cadence in seconds (read live: the settings card edits it in place). */
|
|
493
613
|
const sessionPollSeconds = (): number => readConfigField(config?.sessionPollSeconds, DEFAULT_SESSION_POLL_SECONDS)
|
|
614
|
+
/**
|
|
615
|
+
* Current run-launch admission width. Read live on every admission, so the
|
|
616
|
+
* settings card bounds the next launch of a run that is still queueing without
|
|
617
|
+
* remounting the row; the Host clamps a hand-edited value into the schema range.
|
|
618
|
+
*/
|
|
619
|
+
const runLaunchWidth = (): number => readConfigField(config?.runLaunchWidth, DEFAULT_RUN_LAUNCH_WIDTH)
|
|
494
620
|
/**
|
|
495
621
|
* The baseline the permission confirmation gate judges against: the row's own
|
|
496
622
|
* `sessionDefaultPermission` when the deployment pins one, otherwise the Host's
|
|
@@ -529,19 +655,40 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
529
655
|
* executions that start afterwards.
|
|
530
656
|
*/
|
|
531
657
|
const goalRunEnabled = (): boolean => readConfigField(config?.goalRunEnabled, DEFAULT_GOAL_RUN)
|
|
658
|
+
/**
|
|
659
|
+
* Live reader of the new-card defaults, read at every use like the switch
|
|
660
|
+
* above: the create dialog prefills from the published snapshot, and each
|
|
661
|
+
* creating session re-reads them when it submits the create. A blank value
|
|
662
|
+
* means "not set" and reaches the resolver as an absent target.
|
|
663
|
+
*/
|
|
664
|
+
const cardDefaults = (): CardDefaults => {
|
|
665
|
+
const permission = readConfigField<TaskPermission | ''>(config?.cardDefaultPermission, '')
|
|
666
|
+
return {
|
|
667
|
+
mode: readConfigField(config?.cardDefaultMode, '').trim() || undefined,
|
|
668
|
+
permission: isTaskPermission(permission) ? permission : undefined,
|
|
669
|
+
model: readConfigField(config?.cardDefaultModel, '').trim() || undefined,
|
|
670
|
+
reasoningEffort: readConfigField(config?.cardDefaultReasoningEffort, '').trim() || undefined,
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// The 0.2.1 gateway requires every request to name the peer it speaks for:
|
|
674
|
+
// the board calls as the host-local owner, so its peer is minted on this
|
|
675
|
+
// fiber and aborts with it (see createOwnerPeer).
|
|
676
|
+
const ownerPeer = createOwnerPeer(ctx)
|
|
532
677
|
const host = new TaskBoardHostService(ctx.typertGateway, {
|
|
533
678
|
workspaceRegistry: ctx.workspaceRegistry,
|
|
679
|
+
ownerPeer,
|
|
534
680
|
sessionDefaultPermission,
|
|
535
681
|
maxSubtaskDepth: maxSubtaskDepth(),
|
|
536
682
|
goalRunEnabled,
|
|
683
|
+
runLaunchWidth,
|
|
684
|
+
cardDefaults,
|
|
537
685
|
verificationSettings,
|
|
538
686
|
verificationCatalog,
|
|
539
687
|
timers: resolveHostTimers(ctx),
|
|
540
688
|
team: buildTeamDispatcher(ctx, config?.teamProvider ?? DEFAULT_TEAM_PROVIDER),
|
|
541
689
|
commandDispatcher: {
|
|
542
690
|
async execute(sessionId, line, signal) {
|
|
543
|
-
const agent = ctx
|
|
544
|
-
if (agent === undefined) throw new Error(`execution session ${sessionId} is not available`)
|
|
691
|
+
const agent = await resolveExecutionAgent(ctx, sessionId)
|
|
545
692
|
return (await ctx.commands.execute(agent, line, [], signal))?.result
|
|
546
693
|
},
|
|
547
694
|
},
|