@tt-a1i/hive 2.1.14 → 2.1.15

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 (54) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/src/cli/team.js +19 -6
  3. package/dist/src/server/agent-startup-instructions.d.ts +1 -0
  4. package/dist/src/server/agent-startup-instructions.js +4 -1
  5. package/dist/src/server/hive-team-guidance.js +2 -0
  6. package/dist/src/server/route-types.d.ts +1 -0
  7. package/dist/src/server/routes-team.js +3 -0
  8. package/dist/src/server/routes-workers.d.ts +2 -0
  9. package/dist/src/server/routes-workers.js +112 -0
  10. package/dist/src/server/routes-workspaces.js +4 -85
  11. package/dist/src/server/routes.js +2 -0
  12. package/dist/src/server/runtime-store-contract.d.ts +5 -0
  13. package/dist/src/server/runtime-store.js +2 -0
  14. package/dist/src/server/sqlite-schema-v39.d.ts +2 -0
  15. package/dist/src/server/sqlite-schema-v39.js +6 -0
  16. package/dist/src/server/sqlite-schema.d.ts +1 -1
  17. package/dist/src/server/sqlite-schema.js +8 -1
  18. package/dist/src/server/team-list-serializer.d.ts +5 -1
  19. package/dist/src/server/team-list-serializer.js +2 -1
  20. package/dist/src/server/team-memory-dream-prompt.js +18 -5
  21. package/dist/src/server/team-operations.d.ts +4 -0
  22. package/dist/src/server/team-operations.js +18 -5
  23. package/dist/src/server/workspace-store-contract.d.ts +6 -0
  24. package/dist/src/server/workspace-store-hydration.js +2 -1
  25. package/dist/src/server/workspace-store-support.d.ts +1 -0
  26. package/dist/src/server/workspace-store.js +57 -2
  27. package/dist/src/shared/types.d.ts +4 -0
  28. package/dist/src/shared/worker-avatar.d.ts +2 -0
  29. package/dist/src/shared/worker-avatar.js +55 -0
  30. package/package.json +1 -1
  31. package/web/dist/assets/AddWorkerDialog-Dfg4e74k.js +2 -0
  32. package/web/dist/assets/AddWorkspaceFlow-Bt5i_6p3.js +1 -0
  33. package/web/dist/assets/FirstRunWizard--NAbC5zC.js +1 -0
  34. package/web/dist/assets/{MarketplaceDrawer-BHTvxLMz.js → MarketplaceDrawer-CJhft0Xd.js} +1 -1
  35. package/web/dist/assets/{TaskGraphDrawer-woCXhUwp.js → TaskGraphDrawer-BUCR4zPH.js} +1 -1
  36. package/web/dist/assets/WhatsNewDialog-B05r5jd4.js +1 -0
  37. package/web/dist/assets/WorkerModal-CSM87uaz.js +1 -0
  38. package/web/dist/assets/{WorkflowsDrawer-Diw0TiiJ.js → WorkflowsDrawer-pBezljLE.js} +1 -1
  39. package/web/dist/assets/WorkspaceMemoryDrawer-Akq36Iaz.js +1 -0
  40. package/web/dist/assets/{WorkspaceTaskDrawer-D10RcAUj.js → WorkspaceTaskDrawer-CDhs_vbw.js} +1 -1
  41. package/web/dist/assets/index-BTPjdp2B.js +84 -0
  42. package/web/dist/assets/index-Cs7E1UKt.css +1 -0
  43. package/web/dist/assets/{search-D6bjVJKH.js → search-CWX42x_k.js} +1 -1
  44. package/web/dist/assets/{square-terminal-BUBEjTQZ.js → square-terminal-DoiN9oxG.js} +1 -1
  45. package/web/dist/index.html +2 -2
  46. package/web/dist/sw.js +1 -1
  47. package/web/dist/assets/AddWorkerDialog-BdIc_ZDz.js +0 -2
  48. package/web/dist/assets/AddWorkspaceFlow-h-UH433o.js +0 -1
  49. package/web/dist/assets/FirstRunWizard-ChuB1siy.js +0 -1
  50. package/web/dist/assets/WhatsNewDialog-BcRbRAJI.js +0 -1
  51. package/web/dist/assets/WorkerModal-CTf1k881.js +0 -1
  52. package/web/dist/assets/WorkspaceMemoryDrawer-B7Z1_COx.js +0 -1
  53. package/web/dist/assets/index-BTaZgzo0.js +0 -84
  54. package/web/dist/assets/index-DYIPzuPD.css +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable user-facing changes will be documented in this file.
4
4
 
5
+ ## 2.1.15 - 2026-07-02
6
+
7
+ Worker avatars, terminal rendering reliability, and clearer team report delivery.
8
+
9
+ - Adds custom worker avatars, including upload, crop, validation, persistence,
10
+ and display across the team UI.
11
+ - Keeps worker avatars out of CLI team-list responses while still showing them
12
+ in the browser workspace.
13
+ - Improves terminal rendering under heavy output and reconnect/restore flows,
14
+ reducing stale or duplicated terminal frames.
15
+ - Tightens Dream memory consolidation instructions so procedure references are
16
+ only used for real saved workflows, skills, procedures, templates, or docs.
17
+ - Makes `team report` delivery wording more accurate when Hive accepts a report
18
+ and durable Orchestrator delivery is still in progress.
19
+ - Adjusts Orchestrator guidance so accepted dispatches are usually left alone
20
+ until a member reports, reducing premature status checks or cancels.
21
+
5
22
  ## 2.1.14 - 2026-06-25
6
23
 
7
24
  Terminal focus and Dream memory reliability.
@@ -124,6 +124,23 @@ const GOAL_REPORT_USAGE = 'Usage: team goal report --goal <goal-id> --status pro
124
124
  const GOAL_REPORT_STATUSES = new Set(['progress', 'done', 'blocked', 'failed']);
125
125
  const usageFor = (command) => (command === 'status' ? STATUS_USAGE : REPORT_USAGE);
126
126
  const withUsage = (message, command) => `${message}\n\n${usageFor(command)}`;
127
+ const printOrchestratorDeliveryWarning = (action, payload) => {
128
+ if (payload.delivery_state === 'delivered' || payload.delivery_state === 'delivering')
129
+ return;
130
+ if (payload.delivery_state === 'queued') {
131
+ const detail = payload.forward_error ? `: ${payload.forward_error}` : '.';
132
+ console.error(`Hive recorded the ${action}, but Orchestrator did not receive it in real time. It is queued for durable delivery${detail}`);
133
+ return;
134
+ }
135
+ if (payload.delivery_state === 'failed') {
136
+ const detail = payload.forward_error ? `: ${payload.forward_error}` : '.';
137
+ console.error(`Hive recorded the ${action}, but could not deliver it to Orchestrator or confirm durable queueing${detail}`);
138
+ return;
139
+ }
140
+ if (payload.forwarded === false && payload.forward_error) {
141
+ console.error(`Hive recorded the ${action}, but could not deliver it to Orchestrator in real time: ${payload.forward_error}`);
142
+ }
143
+ };
127
144
  const readGeneratedProtocolGuide = (topic) => {
128
145
  const protocolPath = join(process.cwd(), '.hive', 'PROTOCOL.md');
129
146
  if (!existsSync(protocolPath))
@@ -1043,9 +1060,7 @@ export const runTeamCommand = async (argv) => {
1043
1060
  artifacts: report.artifacts,
1044
1061
  });
1045
1062
  const payload = (await response.json());
1046
- if (payload.forwarded === false && payload.forward_error) {
1047
- console.error(`Hive recorded the status update, but could not deliver it to Orchestrator in real time: ${payload.forward_error}`);
1048
- }
1063
+ printOrchestratorDeliveryWarning('status update', payload);
1049
1064
  if (payload.pending_warning)
1050
1065
  console.error(payload.pending_warning);
1051
1066
  return;
@@ -1064,9 +1079,7 @@ export const runTeamCommand = async (argv) => {
1064
1079
  artifacts: report.artifacts,
1065
1080
  });
1066
1081
  const payload = (await response.json());
1067
- if (payload.forwarded === false && payload.forward_error) {
1068
- console.error(`Hive recorded the report, but could not deliver it to Orchestrator in real time: ${payload.forward_error}`);
1069
- }
1082
+ printOrchestratorDeliveryWarning('report', payload);
1070
1083
  if (payload.pending_warning)
1071
1084
  console.error(payload.pending_warning);
1072
1085
  return;
@@ -9,6 +9,7 @@ export declare const buildAgentLegacyIdentityMarker: ({ agent, workspace, }: {
9
9
  workspace: WorkspaceSummary;
10
10
  }) => string;
11
11
  export declare const WORKER_STARTUP_READY_STATUS = "ready: startup prompt received; Hive CLI channel OK; waiting for dispatch.";
12
+ export declare const REVIEWER_STARTUP_READY_STATUS = "ready: reviewer available as the final review gate. If review finds issues, have them fixed and send the work back to me until no issues remain before reporting completion to the user.";
12
13
  export declare const buildAgentStartupInstructions: ({ agent, memoryDigest, workspace, flags, }: {
13
14
  agent: AgentSummary;
14
15
  memoryDigest?: string | null | undefined;
@@ -5,6 +5,8 @@ import { TASKS_RELATIVE_PATH } from './tasks-file.js';
5
5
  export const buildAgentSessionBindingMarker = ({ agent, workspace, }) => `Hive session binding: workspace_id=${workspace.id}; agent_id=${agent.id}`;
6
6
  export const buildAgentLegacyIdentityMarker = ({ agent, workspace, }) => `You are ${escapeHiveEnvelopeText(agent.name)} (${agent.role}) in workspace ${escapeHiveEnvelopeText(workspace.name)}.`;
7
7
  export const WORKER_STARTUP_READY_STATUS = 'ready: startup prompt received; Hive CLI channel OK; waiting for dispatch.';
8
+ export const REVIEWER_STARTUP_READY_STATUS = 'ready: reviewer available as the final review gate. If review finds issues, have them fixed and send the work back to me until no issues remain before reporting completion to the user.';
9
+ const getStartupReadyStatus = (agent) => agent.role === 'reviewer' ? REVIEWER_STARTUP_READY_STATUS : WORKER_STARTUP_READY_STATUS;
8
10
  export const buildAgentStartupInstructions = ({ agent, memoryDigest, workspace, flags = FEATURE_FLAGS_ALL_OFF, }) => {
9
11
  const { workflowsEnabled } = flags;
10
12
  const lines = [
@@ -35,7 +37,8 @@ export const buildAgentStartupInstructions = ({ agent, memoryDigest, workspace,
35
37
  lines.push('Available team commands:', '- team status "<finding>" [--artifact <path>] escalate a patrol finding to the Orchestrator', '- team status --stdin [--artifact <path>] same, body read from stdin', '- team recall "<query>" [--limit <n>] [--window <n>] search prior team messages/reports in this workspace', '- team memory show <memory-id> inspect a memory entry and its evidence', '- team memory search "<query>" [--limit <n>] search active workspace memory', '- team --help command syntax only', '', 'You observe only: no dispatches are ever assigned to you, and `team report` is not available to your role.', '', 'Hive sentinel boundaries:', ...getHiveTeamRules(agent, flags));
36
38
  }
37
39
  else {
38
- lines.push('Available team commands:', '- team report "<result>" [--dispatch <id>] [--artifact <path>] report done / failed / blocked', '- team report --stdin [--dispatch <id>] [--artifact <path>] same, body read from stdin (multi-line / quotes / special chars)', '- team status "<state>" [--artifact <path>] progress / standby update at any time; never closes a dispatch', '- team status --stdin [--artifact <path>] same, body read from stdin', '- team recall "<query>" [--limit <n>] [--window <n>] search prior team messages/reports in this workspace', '- team memory show <memory-id> inspect a memory entry and its evidence', '- team memory search "<query>" [--limit <n>] search active workspace memory', '- team memory dream show <dream-run-id> inspect a pending Dream run only when the Orchestrator assigns memory review', '- team --help command syntax only; NOT a way to report', '', 'Startup handshake:', `- After reading this startup message, immediately run exactly once: \`team status "${WORKER_STARTUP_READY_STATUS}"\`.`, '- This readiness status tells the Orchestrator this member started successfully; it does not close any dispatch.', '- Do not use `team report` for startup readiness. Use `team report` only after you receive an assigned dispatch task.', '', 'Syntax notes:', '- The body is the first positional argument; flag order is free: `team report "result" --dispatch X` and `team report --dispatch X "result"` are both valid.', "- Long bodies (multi-line / quotes / shell metacharacters) always go through `--stdin`, piped in via your shell (POSIX: a quoted heredoc `<<'EOF' … EOF` to stop $var / backtick expansion; Windows cmd: `type body.txt |` or `< body.txt`; PowerShell: `Get-Content -Raw -Encoding utf8 body.txt |`). `--stdin` only reads from stdin and relies on no shell syntax of its own.", '- On error the CLI also prints USAGE — fix the arguments against it.', '', 'Hive member boundaries:', ...getHiveTeamRules(agent, flags));
40
+ const startupReadyStatus = getStartupReadyStatus(agent);
41
+ lines.push('Available team commands:', '- team report "<result>" [--dispatch <id>] [--artifact <path>] report done / failed / blocked', '- team report --stdin [--dispatch <id>] [--artifact <path>] same, body read from stdin (multi-line / quotes / special chars)', '- team status "<state>" [--artifact <path>] progress / standby update at any time; never closes a dispatch', '- team status --stdin [--artifact <path>] same, body read from stdin', '- team recall "<query>" [--limit <n>] [--window <n>] search prior team messages/reports in this workspace', '- team memory show <memory-id> inspect a memory entry and its evidence', '- team memory search "<query>" [--limit <n>] search active workspace memory', '- team memory dream show <dream-run-id> inspect a pending Dream run only when the Orchestrator assigns memory review', '- team --help command syntax only; NOT a way to report', '', 'Startup handshake:', `- After reading this startup message, immediately run exactly once: \`team status "${startupReadyStatus}"\`.`, '- This readiness status tells the Orchestrator this member started successfully; it does not close any dispatch.', '- Do not use `team report` for startup readiness. Use `team report` only after you receive an assigned dispatch task.', '', 'Syntax notes:', '- The body is the first positional argument; flag order is free: `team report "result" --dispatch X` and `team report --dispatch X "result"` are both valid.', "- Long bodies (multi-line / quotes / shell metacharacters) always go through `--stdin`, piped in via your shell (POSIX: a quoted heredoc `<<'EOF' … EOF` to stop $var / backtick expansion; Windows cmd: `type body.txt |` or `< body.txt`; PowerShell: `Get-Content -Raw -Encoding utf8 body.txt |`). `--stdin` only reads from stdin and relies on no shell syntax of its own.", '- On error the CLI also prints USAGE — fix the arguments against it.', '', 'Hive member boundaries:', ...getHiveTeamRules(agent, flags));
39
42
  }
40
43
  lines.push('', '</hive-message>', '');
41
44
  return lines.join('\n');
@@ -23,6 +23,7 @@ export const buildOrchestratorReminderTail = ({ workflowsEnabled }) => {
23
23
  '(a) `team list`, then `team send "<member-name>" "<task>"` to dispatch to a suitable existing member by name. Prefer user-created/user-managed members already in the current team; stale names fail. ' +
24
24
  `If no existing member fits, a member is stopped/queued/not prompt-ready, or independent branches need more members, report that condition and recommend what member the user may start or add; do not take a branch yourself. Use \`team spawn <role> [--cli <${BUILTIN_COMMAND_PRESET_CLI_LIST}>] [--ephemeral]\` only when the user explicitly authorizes it or workspace policy permits autonomous staffing for this task. ` +
25
25
  '(b) `team cancel --dispatch <id> "<reason>"` to close an obsolete dispatch. ' +
26
+ 'Accepted dispatches should normally be left alone until the member reports. Do not cancel or send status checks unless progress is genuinely overdue or the user asks. ' +
26
27
  (workflowsEnabled
27
28
  ? '(c) `team workflow run --stdin` to fan out across 3+ members or run a staged review→fix — never a loop of `team send` (no barrier, no UI group, no stop button). (d) plain text only for clarification, status/final synthesis, or the explicitly allowed trivial check. '
28
29
  : '(c) plain text only for clarification, status/final synthesis, or the explicitly allowed trivial check. ') +
@@ -58,6 +59,7 @@ const CORE_ORCHESTRATOR_RULES = [
58
59
  'Do not keep a non-trivial parallel branch for yourself just to avoid dispatching.',
59
60
  'If a dispatch must be changed or cancelled, use `team cancel --dispatch <id> "<reason>"`, then resend the full updated task if needed.',
60
61
  'Each `team send` creates a separate dispatch and requires a separate `team report`.',
62
+ 'Accepted dispatches should normally be left alone until the member reports. Do not cancel or send status checks unless progress is genuinely overdue or the user asks.',
61
63
  'Treat stopped, queued, delivery-failed, and prompt-readiness-timeout as current-team runtime conditions to report or retry, not proof that the member is unsuitable.',
62
64
  '`team spawn` is persistent by default. When authorized to create task-scoped capacity, prefer `--ephemeral` unless the user asks for persistent capacity.',
63
65
  'All Hive members in one workspace share the same filesystem root with no per-member isolation. Split dispatches so no two members edit the same files/modules at the same time; if work would collide, serialize it or assign one owner.',
@@ -118,6 +118,7 @@ export interface CreateWorkspaceBody {
118
118
  }
119
119
  export interface CreateWorkerBody {
120
120
  autostart?: boolean;
121
+ avatar?: string | null;
121
122
  command_preset_id?: string | null;
122
123
  description?: string;
123
124
  name: string;
@@ -340,6 +340,7 @@ export const teamRoutes = [
340
340
  status,
341
341
  });
342
342
  sendJson(response, 202, {
343
+ delivery_state: result.deliveryState,
343
344
  dispatch_id: result.dispatch?.id ?? null,
344
345
  forward_error: result.forwardError,
345
346
  forwarded: result.forwarded,
@@ -351,6 +352,7 @@ export const teamRoutes = [
351
352
  else {
352
353
  const result = store.reportTask(projectId, fromAgentId, reportInput);
353
354
  sendJson(response, 202, {
355
+ delivery_state: result.deliveryState,
354
356
  dispatch_id: result.dispatch?.id ?? null,
355
357
  forward_error: result.forwardError,
356
358
  forwarded: result.forwarded,
@@ -379,6 +381,7 @@ export const teamRoutes = [
379
381
  text: resultText,
380
382
  });
381
383
  sendJson(response, 202, {
384
+ delivery_state: result.deliveryState,
382
385
  dispatch_id: result.dispatch?.id ?? null,
383
386
  forward_error: result.forwardError,
384
387
  forwarded: result.forwarded,
@@ -0,0 +1,2 @@
1
+ import type { RouteDefinition } from './route-types.js';
2
+ export declare const workerRoutes: RouteDefinition[];
@@ -0,0 +1,112 @@
1
+ import { normalizeWorkerAvatar } from '../shared/worker-avatar.js';
2
+ import { resolveCommandPresetLaunchConfig, resolveStartupCommandLaunchConfig, } from './agent-launch-resolver.js';
3
+ import { BadRequestError } from './http-errors.js';
4
+ import { autostartAgent } from './orchestrator-autostart.js';
5
+ import { getDefaultRoleDescription } from './role-templates.js';
6
+ import { getRequiredParam, readJsonBody, route, sendJson } from './route-helpers.js';
7
+ import { enrichTeamList } from './team-list-enrichment.js';
8
+ import { serializeTeamListItem } from './team-list-serializer.js';
9
+ import { requireUiTokenFromRequest } from './ui-auth-helpers.js';
10
+ import { resolveWorkspaceUiLanguage } from './workspace-ui-language.js';
11
+ const getRuntimePort = (request) => String(request.socket.localPort ?? '');
12
+ const readWorkerAvatar = (value) => {
13
+ try {
14
+ return normalizeWorkerAvatar(value);
15
+ }
16
+ catch (error) {
17
+ throw new BadRequestError(error instanceof Error ? error.message : String(error));
18
+ }
19
+ };
20
+ const getSerializedWorker = (workspaceId, workerId, store) => {
21
+ const worker = store.listWorkers(workspaceId).find((item) => item.id === workerId);
22
+ if (!worker) {
23
+ throw new Error(`Worker not found: ${workerId}`);
24
+ }
25
+ const [enriched] = enrichTeamList(workspaceId, store, [worker]);
26
+ if (!enriched)
27
+ throw new Error(`Worker enrichment failed: ${workerId}`);
28
+ return serializeTeamListItem(enriched, undefined, { includeAvatar: true });
29
+ };
30
+ export const workerRoutes = [
31
+ route('POST', '/api/workspaces/:workspaceId/workers', async ({ params, request, response, store }) => {
32
+ const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
33
+ if (!workspaceId) {
34
+ return;
35
+ }
36
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
37
+ const body = await readJsonBody(request);
38
+ const avatar = Object.hasOwn(body, 'avatar') ? readWorkerAvatar(body.avatar) : null;
39
+ const presetId = body.command_preset_id ?? null;
40
+ const startupCommand = typeof body.startup_command === 'string' ? body.startup_command : null;
41
+ const language = resolveWorkspaceUiLanguage(store.settings, workspaceId, body.ui_language);
42
+ const launchConfig = startupCommand?.trim()
43
+ ? resolveStartupCommandLaunchConfig(store.settings, startupCommand, presetId)
44
+ : presetId
45
+ ? resolveCommandPresetLaunchConfig(store.settings, presetId)
46
+ : undefined;
47
+ if (presetId && !startupCommand?.trim() && !launchConfig) {
48
+ throw new Error(`Command preset not found: ${presetId}`);
49
+ }
50
+ const worker = store.addWorker(workspaceId, {
51
+ ...body,
52
+ avatar,
53
+ description: typeof body.description === 'string'
54
+ ? body.description
55
+ : getDefaultRoleDescription(body.role, language),
56
+ });
57
+ if (launchConfig) {
58
+ try {
59
+ store.configureAgentLaunch(workspaceId, worker.id, launchConfig);
60
+ }
61
+ catch (error) {
62
+ store.deleteWorker(workspaceId, worker.id);
63
+ throw error;
64
+ }
65
+ }
66
+ const agentStart = body.autostart === true
67
+ ? await autostartAgent(store, workspaceId, worker.id, getRuntimePort(request), {
68
+ missingConfigError: 'No worker launch config available',
69
+ })
70
+ : { ok: false, error: null, run_id: null };
71
+ sendJson(response, 201, {
72
+ ...getSerializedWorker(workspaceId, worker.id, store),
73
+ agent_start: agentStart,
74
+ });
75
+ }),
76
+ route('DELETE', '/api/workspaces/:workspaceId/workers/:workerId', ({ params, request, response, store }) => {
77
+ const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id and worker id are required');
78
+ const workerId = getRequiredParam(response, params, 'workerId', 'Workspace id and worker id are required');
79
+ if (!workspaceId || !workerId) {
80
+ return;
81
+ }
82
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
83
+ store.deleteWorker(workspaceId, workerId);
84
+ response.statusCode = 204;
85
+ response.end();
86
+ }),
87
+ route('PATCH', '/api/workspaces/:workspaceId/workers/:workerId', async ({ params, request, response, store }) => {
88
+ const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id and worker id are required');
89
+ const workerId = getRequiredParam(response, params, 'workerId', 'Workspace id and worker id are required');
90
+ if (!workspaceId || !workerId) {
91
+ return;
92
+ }
93
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
94
+ const body = await readJsonBody(request);
95
+ const hasName = Object.hasOwn(body, 'name');
96
+ const hasAvatar = Object.hasOwn(body, 'avatar');
97
+ if (!hasName && !hasAvatar) {
98
+ sendJson(response, 400, { error: 'name or avatar is required' });
99
+ return;
100
+ }
101
+ if (hasName && typeof body.name !== 'string') {
102
+ sendJson(response, 400, { error: 'name must be a string' });
103
+ return;
104
+ }
105
+ const avatar = hasAvatar ? readWorkerAvatar(body.avatar) : undefined;
106
+ store.updateWorkerProfile(workspaceId, workerId, {
107
+ ...(hasName ? { name: body.name } : {}),
108
+ ...(hasAvatar ? { avatar: avatar ?? null } : {}),
109
+ });
110
+ sendJson(response, 200, getSerializedWorker(workspaceId, workerId, store));
111
+ }),
112
+ ];
@@ -1,8 +1,6 @@
1
- import { resolveCommandPresetLaunchConfig, resolveStartupCommandLaunchConfig, } from './agent-launch-resolver.js';
2
1
  import { PtyInactiveError } from './http-errors.js';
3
- import { autostartAgent, autostartOrchestrator } from './orchestrator-autostart.js';
2
+ import { autostartOrchestrator } from './orchestrator-autostart.js';
4
3
  import { seedOrchestratorLaunchConfig } from './orchestrator-launch.js';
5
- import { getDefaultRoleDescription } from './role-templates.js';
6
4
  import { getRequiredParam, readJsonBody, route, sendJson } from './route-helpers.js';
7
5
  import { authenticateCliAgent, requireCommandForRole } from './team-authz.js';
8
6
  import { enrichTeamList } from './team-list-enrichment.js';
@@ -15,7 +13,7 @@ import { resolveWorkspaceUiLanguage, writeWorkspaceUiLanguage } from './workspac
15
13
  into the team list payload. Display-only ages — no timeout or heartbeat is
16
14
  derived from them; `queued` rows age from createdAt (submittedAt is null
17
15
  until delivery). */
18
- const serializeTeamListWithOpenDispatches = (store, workspaceId) => {
16
+ const serializeTeamListWithOpenDispatches = (store, workspaceId, options = {}) => {
19
17
  const now = Date.now();
20
18
  const workflowAgentId = getWorkflowAgentId(workspaceId);
21
19
  const openByWorker = new Map();
@@ -35,17 +33,7 @@ const serializeTeamListWithOpenDispatches = (store, workspaceId) => {
35
33
  });
36
34
  openByWorker.set(dispatch.toAgentId, list);
37
35
  }
38
- return enrichTeamList(workspaceId, store, store.listWorkers(workspaceId)).map((worker) => serializeTeamListItem(worker, openByWorker.get(worker.id)));
39
- };
40
- const getSerializedWorker = (workspaceId, workerId, store) => {
41
- const worker = store.listWorkers(workspaceId).find((item) => item.id === workerId);
42
- if (!worker) {
43
- throw new Error(`Worker not found: ${workerId}`);
44
- }
45
- const [enriched] = enrichTeamList(workspaceId, store, [worker]);
46
- if (!enriched)
47
- throw new Error(`Worker enrichment failed: ${workerId}`);
48
- return serializeTeamListItem(enriched);
36
+ return enrichTeamList(workspaceId, store, store.listWorkers(workspaceId)).map((worker) => serializeTeamListItem(worker, openByWorker.get(worker.id), options));
49
37
  };
50
38
  const getRuntimePort = (request) => String(request.socket.localPort ?? '');
51
39
  export const workspaceRoutes = [
@@ -92,7 +80,7 @@ export const workspaceRoutes = [
92
80
  return;
93
81
  }
94
82
  requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
95
- sendJson(response, 200, serializeTeamListWithOpenDispatches(store, workspaceId));
83
+ sendJson(response, 200, serializeTeamListWithOpenDispatches(store, workspaceId, { includeAvatar: true }));
96
84
  }),
97
85
  route('GET', '/api/workspaces/:workspaceId/team', ({ params, request, response, store }) => {
98
86
  const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
@@ -114,75 +102,6 @@ export const workspaceRoutes = [
114
102
  store.drainReportOutbox(workspaceId, agent.id);
115
103
  sendJson(response, 200, serializeTeamListWithOpenDispatches(store, workspaceId));
116
104
  }),
117
- route('POST', '/api/workspaces/:workspaceId/workers', async ({ params, request, response, store }) => {
118
- const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
119
- if (!workspaceId) {
120
- return;
121
- }
122
- requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
123
- const body = await readJsonBody(request);
124
- const presetId = body.command_preset_id ?? null;
125
- const startupCommand = typeof body.startup_command === 'string' ? body.startup_command : null;
126
- const language = resolveWorkspaceUiLanguage(store.settings, workspaceId, body.ui_language);
127
- const launchConfig = startupCommand?.trim()
128
- ? resolveStartupCommandLaunchConfig(store.settings, startupCommand, presetId)
129
- : presetId
130
- ? resolveCommandPresetLaunchConfig(store.settings, presetId)
131
- : undefined;
132
- if (presetId && !startupCommand?.trim() && !launchConfig) {
133
- throw new Error(`Command preset not found: ${presetId}`);
134
- }
135
- const worker = store.addWorker(workspaceId, {
136
- ...body,
137
- description: typeof body.description === 'string'
138
- ? body.description
139
- : getDefaultRoleDescription(body.role, language),
140
- });
141
- if (launchConfig) {
142
- try {
143
- store.configureAgentLaunch(workspaceId, worker.id, launchConfig);
144
- }
145
- catch (error) {
146
- store.deleteWorker(workspaceId, worker.id);
147
- throw error;
148
- }
149
- }
150
- const agentStart = body.autostart === true
151
- ? await autostartAgent(store, workspaceId, worker.id, getRuntimePort(request), {
152
- missingConfigError: 'No worker launch config available',
153
- })
154
- : { ok: false, error: null, run_id: null };
155
- sendJson(response, 201, {
156
- ...getSerializedWorker(workspaceId, worker.id, store),
157
- agent_start: agentStart,
158
- });
159
- }),
160
- route('DELETE', '/api/workspaces/:workspaceId/workers/:workerId', ({ params, request, response, store }) => {
161
- const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id and worker id are required');
162
- const workerId = getRequiredParam(response, params, 'workerId', 'Workspace id and worker id are required');
163
- if (!workspaceId || !workerId) {
164
- return;
165
- }
166
- requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
167
- store.deleteWorker(workspaceId, workerId);
168
- response.statusCode = 204;
169
- response.end();
170
- }),
171
- route('PATCH', '/api/workspaces/:workspaceId/workers/:workerId', async ({ params, request, response, store }) => {
172
- const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id and worker id are required');
173
- const workerId = getRequiredParam(response, params, 'workerId', 'Workspace id and worker id are required');
174
- if (!workspaceId || !workerId) {
175
- return;
176
- }
177
- requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
178
- const body = await readJsonBody(request);
179
- if (typeof body.name !== 'string') {
180
- sendJson(response, 400, { error: 'name is required' });
181
- return;
182
- }
183
- store.renameWorker(workspaceId, workerId, body.name);
184
- sendJson(response, 200, getSerializedWorker(workspaceId, workerId, store));
185
- }),
186
105
  route('POST', '/api/workspaces/:workspaceId/user-input', async ({ params, request, response, store }) => {
187
106
  const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
188
107
  if (!workspaceId) {
@@ -17,6 +17,7 @@ import { teamMemoryRoutes } from './routes-team-memory.js';
17
17
  import { teamRecallRoutes } from './routes-team-recall.js';
18
18
  import { uiRoutes } from './routes-ui.js';
19
19
  import { versionRoutes } from './routes-version.js';
20
+ import { workerRoutes } from './routes-workers.js';
20
21
  import { workflowScheduleRoutes } from './routes-workflow-schedules.js';
21
22
  import { workflowRoutes } from './routes-workflows.js';
22
23
  import { workspaceMemoryRoutes } from './routes-workspace-memory.js';
@@ -25,6 +26,7 @@ import { workspaceUploadRoutes } from './routes-workspace-uploads.js';
25
26
  import { workspaceRoutes } from './routes-workspaces.js';
26
27
  const routes = [
27
28
  ...workspaceRoutes,
29
+ ...workerRoutes,
28
30
  ...scenarioRoutes,
29
31
  ...workspaceUploadRoutes,
30
32
  ...openWorkspaceRoutes,
@@ -34,6 +34,11 @@ export interface RuntimeStore {
34
34
  addWorker: (workspaceId: string, input: WorkerInput) => AgentSummary;
35
35
  addWorkerWithLaunch: (workspaceId: string, input: WorkerInput, launchConfig: AgentLaunchConfigInput) => AgentSummary;
36
36
  deleteWorker: (workspaceId: string, workerId: string) => void;
37
+ updateWorkerProfile: (workspaceId: string, workerId: string, input: {
38
+ avatar?: string | null;
39
+ name?: string;
40
+ }) => AgentSummary;
41
+ updateWorkerAvatar: (workspaceId: string, workerId: string, avatar: string | null) => AgentSummary;
37
42
  renameWorker: (workspaceId: string, workerId: string, name: string) => AgentSummary;
38
43
  recordUserInput: (workspaceId: string, orchestratorId: string, text: string) => void;
39
44
  deliverUserInput: (workspaceId: string, orchestratorId: string, text: string) => Promise<void>;
@@ -81,6 +81,8 @@ export const createRuntimeStore = (options = {}) => {
81
81
  },
82
82
  addWorker: (workspaceId, input) => services.workspaceStore.addWorker(workspaceId, input),
83
83
  addWorkerWithLaunch: workerMutations.addWorkerWithLaunch,
84
+ updateWorkerProfile: (workspaceId, workerId, input) => services.workspaceStore.updateWorkerProfile(workspaceId, workerId, input),
85
+ updateWorkerAvatar: (workspaceId, workerId, avatar) => services.workspaceStore.updateWorkerAvatar(workspaceId, workerId, avatar),
84
86
  renameWorker: (workspaceId, workerId, name) => services.workspaceStore.renameWorker(workspaceId, workerId, name),
85
87
  deleteWorker: workerMutations.deleteWorker,
86
88
  recordUserInput: services.teamOps.recordUserInput,
@@ -0,0 +1,2 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export declare const applySchemaVersion39: (db: Database) => void;
@@ -0,0 +1,6 @@
1
+ export const applySchemaVersion39 = (db) => {
2
+ const workerColumns = new Set(db.prepare('PRAGMA table_info(workers)').all().map((column) => column.name));
3
+ if (!workerColumns.has('avatar')) {
4
+ db.exec('ALTER TABLE workers ADD COLUMN avatar TEXT');
5
+ }
6
+ };
@@ -1,3 +1,3 @@
1
1
  import type { Database } from 'better-sqlite3';
2
- export declare const CURRENT_SCHEMA_VERSION = 38;
2
+ export declare const CURRENT_SCHEMA_VERSION = 39;
3
3
  export declare const initializeRuntimeDatabase: (db: Database) => void;
@@ -31,7 +31,8 @@ import { applySchemaVersion35 } from './sqlite-schema-v35.js';
31
31
  import { applySchemaVersion36 } from './sqlite-schema-v36.js';
32
32
  import { applySchemaVersion37 } from './sqlite-schema-v37.js';
33
33
  import { applySchemaVersion38 } from './sqlite-schema-v38.js';
34
- export const CURRENT_SCHEMA_VERSION = 38;
34
+ import { applySchemaVersion39 } from './sqlite-schema-v39.js';
35
+ export const CURRENT_SCHEMA_VERSION = 39;
35
36
  // Idempotent column-add helper. SQLite doesn't have `ALTER TABLE … ADD COLUMN
36
37
  // IF NOT EXISTS`, so PRAGMA-check first. Safe to call on every init; required
37
38
  // for foreign-built DBs where the version-gated migration is skipped.
@@ -60,6 +61,7 @@ export const initializeRuntimeDatabase = (db) => {
60
61
  workspace_id TEXT NOT NULL,
61
62
  name TEXT NOT NULL,
62
63
  description TEXT,
64
+ avatar TEXT,
63
65
  last_session_id TEXT,
64
66
  role TEXT NOT NULL,
65
67
  created_at INTEGER NOT NULL
@@ -239,6 +241,7 @@ export const initializeRuntimeDatabase = (db) => {
239
241
  // migration leaves these absent. PRAGMA + ALTER here makes the writes safe.
240
242
  ensureColumn(db, 'workers', 'ephemeral', 'INTEGER NOT NULL DEFAULT 0');
241
243
  ensureColumn(db, 'workers', 'spawned_by', 'TEXT');
244
+ ensureColumn(db, 'workers', 'avatar', 'TEXT');
242
245
  ensureColumn(db, 'dispatches', 'workflow_run_id', 'TEXT');
243
246
  ensureColumn(db, 'dispatches', 'step_index', 'INTEGER');
244
247
  // M9 — phase + label on dispatches so the workflow UI can render the
@@ -449,4 +452,8 @@ export const initializeRuntimeDatabase = (db) => {
449
452
  if (!appliedVersions.has(38)) {
450
453
  db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(38, Date.now());
451
454
  }
455
+ applySchemaVersion39(db);
456
+ if (!appliedVersions.has(39)) {
457
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(39, Date.now());
458
+ }
452
459
  };
@@ -1,2 +1,6 @@
1
1
  import type { TeamListItem, TeamListItemPayload, TeamListOpenDispatchPayload } from '../shared/types.js';
2
- export declare const serializeTeamListItem: ({ commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }: TeamListItem, openDispatches?: TeamListOpenDispatchPayload[]) => TeamListItemPayload;
2
+ type SerializeTeamListItemOptions = {
3
+ includeAvatar?: boolean;
4
+ };
5
+ export declare const serializeTeamListItem: ({ avatar, commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }: TeamListItem, openDispatches?: TeamListOpenDispatchPayload[], options?: SerializeTeamListItemOptions) => TeamListItemPayload;
6
+ export {};
@@ -1,9 +1,10 @@
1
- export const serializeTeamListItem = ({ commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }, openDispatches) => ({
1
+ export const serializeTeamListItem = ({ avatar, commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }, openDispatches, options = {}) => ({
2
2
  id,
3
3
  name,
4
4
  role,
5
5
  status,
6
6
  pending_task_count: pendingTaskCount,
7
+ ...(options.includeAvatar === true && avatar ? { avatar } : {}),
7
8
  last_pty_line: lastPtyLine ?? null,
8
9
  command_preset_id: commandPresetId ?? null,
9
10
  ...(ephemeral === true ? { ephemeral: true } : {}),
@@ -1,3 +1,4 @@
1
+ import { DREAM_MEMORY_BODY_MAX_CHARS, DREAM_MEMORY_MAX_ADDS_PER_RUN, MEMORY_PROCEDURE_REF_ID_MAX_CHARS, MEMORY_PROCEDURE_REF_TITLE_MAX_CHARS, MEMORY_TAG_MAX_CHARS, MEMORY_TAG_MAX_COUNT, } from '../shared/team-memory.js';
1
2
  import { escapeHiveEnvelopeAttribute, escapeHiveEnvelopeText } from './hive-envelope-escape.js';
2
3
  export const DREAM_PROMPT_MESSAGE_LIMIT = 200;
3
4
  export const DREAM_PROMPT_MEMORY_LIMIT = 100;
@@ -38,14 +39,25 @@ export const buildDreamPrompt = (input) => [
38
39
  'Read only the protocol messages and memory entries below.',
39
40
  'Return strict JSON only, with shape {"ops":[...]} and no prose.',
40
41
  '',
42
+ 'Output contract (runtime-validated):',
43
+ '- Return exactly {"ops":[...]} as JSON. No markdown, no prose.',
44
+ `- Add no more than ${DREAM_MEMORY_MAX_ADDS_PER_RUN} memories per run.`,
45
+ `- For add/rewrite/merge, body is required and must be <= ${DREAM_MEMORY_BODY_MAX_CHARS} characters. Prefer one compact sentence.`,
46
+ `- If any proposed body would exceed ${DREAM_MEMORY_BODY_MAX_CHARS} characters, shorten it before returning JSON. Never emit an over-limit body.`,
47
+ `- tags must be an array of <= ${MEMORY_TAG_MAX_COUNT} non-empty strings, each <= ${MEMORY_TAG_MAX_CHARS} characters.`,
48
+ '- confidence, when present, must be a number from 0 to 1.',
49
+ '- sources, when present, must be [{"sequence": <positive integer>}] from the protocol window.',
50
+ `- procedure_ref.id must be <= ${MEMORY_PROCEDURE_REF_ID_MAX_CHARS} characters; procedure_ref.title, when present, must be <= ${MEMORY_PROCEDURE_REF_TITLE_MAX_CHARS} characters.`,
51
+ '- For rewrite/merge, omit kind, tags, confidence, and procedure_ref to preserve current values. Use procedure_ref:null only when intentionally clearing an existing ref; provide a structured procedure_ref when the resulting kind is procedure_ref.',
52
+ '',
41
53
  'Allowed ops:',
42
- '- {"op":"add","kind":"fact|preference|decision|pitfall","body":"...","tags":[],"confidence":0.0-1.0,"sources":[{"sequence":123}]}',
43
- '- {"op":"add","kind":"procedure_ref","body":"...","tags":[],"confidence":0.0-1.0,"sources":[{"sequence":123}],"procedure_ref":{"type":"workflow|skill|procedure|template|doc","id":"...","title":"..."}}',
44
- '- {"op":"rewrite","id":"<memory-id>","body":"...","tags":[],"procedure_ref":null}',
54
+ `- {"op":"add","kind":"fact|preference|decision|pitfall","body":"<=${DREAM_MEMORY_BODY_MAX_CHARS} chars","tags":[],"confidence":0.0-1.0,"sources":[{"sequence":123}]}`,
55
+ `- {"op":"add","kind":"procedure_ref","body":"<=${DREAM_MEMORY_BODY_MAX_CHARS} chars","tags":[],"confidence":0.0-1.0,"sources":[{"sequence":123}],"procedure_ref":{"type":"workflow|skill|procedure|template|doc","id":"<=${MEMORY_PROCEDURE_REF_ID_MAX_CHARS} chars","title":"<=${MEMORY_PROCEDURE_REF_TITLE_MAX_CHARS} chars"}}`,
56
+ `- {"op":"rewrite","id":"<memory-id>","body":"<=${DREAM_MEMORY_BODY_MAX_CHARS} chars"}`,
45
57
  '- {"op":"archive","id":"<memory-id>","reason":"..."}',
46
- '- {"op":"merge","into":"<memory-id>","from":["<memory-id>"],"body":"..."}',
58
+ `- {"op":"merge","into":"<memory-id>","from":["<memory-id>"],"body":"<=${DREAM_MEMORY_BODY_MAX_CHARS} chars"}`,
47
59
  '',
48
- 'Rules: add at most 10 entries; every body must be 500 characters or fewer; archive never deletes; if nothing is worth remembering, return {"ops":[]}.',
60
+ `Rules: add at most ${DREAM_MEMORY_MAX_ADDS_PER_RUN} entries; archive never deletes; if nothing is worth remembering, return {"ops":[]}.`,
49
61
  'Do not use kind "procedure_ref" for ordinary workflow advice, command sequences, or procedural lessons. Use fact, decision, preference, or pitfall instead.',
50
62
  'Use kind "procedure_ref" only when the memory points to a durable existing workflow/skill/procedure/template/doc identity, and always include procedure_ref.type plus procedure_ref.id. Keep body as a short reason to consult it.',
51
63
  'Use protocol evidence only: report text, dispatch status, user input, and artifacts. Do not trust self-praise without a concrete report or failure signal.',
@@ -88,6 +100,7 @@ export const buildDreamMaintenancePayload = (run) => [
88
100
  '- Members may run team memory dream show only when you explicitly assign Dream review; only the orchestrator may run team memory apply.',
89
101
  '- Do not edit .hive/memory.md directly and do not use team memory add/forget for this Dream run.',
90
102
  '- If nothing is worth remembering, apply {"ops":[]}.',
103
+ `- Follow the Dream output contract from dream show; every submitted body must be ${DREAM_MEMORY_BODY_MAX_CHARS} characters or fewer.`,
91
104
  '- The runtime will validate the run id, workspace, source window, operation shape, and touched memory ids transactionally.',
92
105
  '',
93
106
  '</hive-system-memory-maintenance>',
@@ -88,7 +88,9 @@ export interface CancelTaskInput {
88
88
  fromAgentId: string;
89
89
  reason: string;
90
90
  }
91
+ export type ReportDeliveryState = 'delivered' | 'delivering' | 'queued' | 'failed';
91
92
  export interface ReportTaskResult {
93
+ deliveryState?: ReportDeliveryState;
92
94
  dispatch: DispatchRecord | null;
93
95
  forwardError: string | null;
94
96
  forwarded: boolean;
@@ -121,11 +123,13 @@ export declare const createTeamOperations: ({ agentRuntime, createDispatch, dele
121
123
  dispatch: null;
122
124
  forwardError: string | null;
123
125
  forwarded: boolean;
126
+ deliveryState?: ReportDeliveryState;
124
127
  };
125
128
  reportTask(workspaceId: string, workerId: string, input?: ReportTaskInput): {
126
129
  pendingWarning?: string;
127
130
  dispatch: DispatchRecord;
128
131
  forwardError: string | null;
129
132
  forwarded: boolean;
133
+ deliveryState?: ReportDeliveryState;
130
134
  };
131
135
  };