2026-01-06 08:41:45 +01:00
|
|
|
import { Type } from "@sinclair/typebox";
|
2026-02-01 10:03:47 +09:00
|
|
|
import crypto from "node:crypto";
|
|
|
|
|
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
|
|
|
|
|
import type { AnyAgentTool } from "./common.js";
|
2026-01-18 00:14:02 +00:00
|
|
|
import { formatThinkingLevels, normalizeThinkLevel } from "../../auto-reply/thinking.js";
|
2026-01-06 08:41:45 +01:00
|
|
|
import { loadConfig } from "../../config/config.js";
|
|
|
|
|
import { callGateway } from "../../gateway/call.js";
|
2026-01-06 18:25:37 +00:00
|
|
|
import {
|
|
|
|
|
isSubagentSessionKey,
|
|
|
|
|
normalizeAgentId,
|
|
|
|
|
parseAgentSessionKey,
|
|
|
|
|
} from "../../routing/session-key.js";
|
2026-01-17 03:57:59 +00:00
|
|
|
import { normalizeDeliveryContext } from "../../utils/delivery-context.js";
|
2026-01-08 06:55:28 +00:00
|
|
|
import { resolveAgentConfig } from "../agent-scope.js";
|
2026-01-09 23:00:23 +01:00
|
|
|
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
|
2026-01-13 06:28:09 +00:00
|
|
|
import { optionalStringEnum } from "../schema/typebox.js";
|
2026-01-07 16:14:25 +00:00
|
|
|
import { buildSubagentSystemPrompt } from "../subagent-announce.js";
|
|
|
|
|
import { registerSubagentRun } from "../subagent-registry.js";
|
2026-01-06 08:41:45 +01:00
|
|
|
import { jsonResult, readStringParam } from "./common.js";
|
|
|
|
|
import {
|
|
|
|
|
resolveDisplaySessionKey,
|
|
|
|
|
resolveInternalSessionKey,
|
|
|
|
|
resolveMainSessionAlias,
|
|
|
|
|
} from "./sessions-helpers.js";
|
|
|
|
|
|
|
|
|
|
const SessionsSpawnToolSchema = Type.Object({
|
|
|
|
|
task: Type.String(),
|
|
|
|
|
label: Type.Optional(Type.String()),
|
2026-01-08 06:55:28 +00:00
|
|
|
agentId: Type.Optional(Type.String()),
|
2026-01-06 22:30:29 +00:00
|
|
|
model: Type.Optional(Type.String()),
|
2026-01-18 00:14:02 +00:00
|
|
|
thinking: Type.Optional(Type.String()),
|
2026-01-10 07:42:32 +13:00
|
|
|
runTimeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
2026-01-07 16:14:25 +00:00
|
|
|
// Back-compat alias. Prefer runTimeoutSeconds.
|
2026-01-10 07:42:32 +13:00
|
|
|
timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
2026-01-13 06:28:09 +00:00
|
|
|
cleanup: optionalStringEnum(["delete", "keep"] as const),
|
2026-01-06 08:41:45 +01:00
|
|
|
});
|
|
|
|
|
|
2026-01-18 00:14:02 +00:00
|
|
|
function splitModelRef(ref?: string) {
|
2026-01-31 16:19:20 +09:00
|
|
|
if (!ref) {
|
|
|
|
|
return { provider: undefined, model: undefined };
|
|
|
|
|
}
|
2026-01-18 00:14:02 +00:00
|
|
|
const trimmed = ref.trim();
|
2026-01-31 16:19:20 +09:00
|
|
|
if (!trimmed) {
|
|
|
|
|
return { provider: undefined, model: undefined };
|
|
|
|
|
}
|
2026-01-18 00:14:02 +00:00
|
|
|
const [provider, model] = trimmed.split("/", 2);
|
2026-01-31 16:19:20 +09:00
|
|
|
if (model) {
|
|
|
|
|
return { provider, model };
|
|
|
|
|
}
|
2026-01-18 00:14:02 +00:00
|
|
|
return { provider: undefined, model: trimmed };
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 18:08:16 +00:00
|
|
|
function normalizeModelSelection(value: unknown): string | undefined {
|
|
|
|
|
if (typeof value === "string") {
|
|
|
|
|
const trimmed = value.trim();
|
|
|
|
|
return trimmed || undefined;
|
|
|
|
|
}
|
2026-01-31 16:19:20 +09:00
|
|
|
if (!value || typeof value !== "object") {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2026-01-12 18:08:16 +00:00
|
|
|
const primary = (value as { primary?: unknown }).primary;
|
2026-01-31 16:19:20 +09:00
|
|
|
if (typeof primary === "string" && primary.trim()) {
|
|
|
|
|
return primary.trim();
|
|
|
|
|
}
|
2026-01-12 18:08:16 +00:00
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 08:41:45 +01:00
|
|
|
export function createSessionsSpawnTool(opts?: {
|
|
|
|
|
agentSessionKey?: string;
|
2026-01-13 06:16:43 +00:00
|
|
|
agentChannel?: GatewayMessageChannel;
|
2026-01-17 02:09:32 +00:00
|
|
|
agentAccountId?: string;
|
2026-01-20 17:22:07 +00:00
|
|
|
agentTo?: string;
|
|
|
|
|
agentThreadId?: string | number;
|
2026-01-24 05:49:23 +00:00
|
|
|
agentGroupId?: string | null;
|
|
|
|
|
agentGroupChannel?: string | null;
|
|
|
|
|
agentGroupSpace?: string | null;
|
2026-01-06 08:40:21 +00:00
|
|
|
sandboxed?: boolean;
|
2026-01-25 06:10:48 -07:00
|
|
|
/** Explicit agent ID override for cron/hook sessions where session key parsing may not work. */
|
|
|
|
|
requesterAgentIdOverride?: string;
|
2026-01-06 08:41:45 +01:00
|
|
|
}): AnyAgentTool {
|
|
|
|
|
return {
|
|
|
|
|
label: "Sessions",
|
|
|
|
|
name: "sessions_spawn",
|
|
|
|
|
description:
|
|
|
|
|
"Spawn a background sub-agent run in an isolated session and announce the result back to the requester chat.",
|
|
|
|
|
parameters: SessionsSpawnToolSchema,
|
|
|
|
|
execute: async (_toolCallId, args) => {
|
|
|
|
|
const params = args as Record<string, unknown>;
|
|
|
|
|
const task = readStringParam(params, "task", { required: true });
|
|
|
|
|
const label = typeof params.label === "string" ? params.label.trim() : "";
|
2026-01-08 06:55:28 +00:00
|
|
|
const requestedAgentId = readStringParam(params, "agentId");
|
2026-01-12 18:08:16 +00:00
|
|
|
const modelOverride = readStringParam(params, "model");
|
2026-01-18 00:14:02 +00:00
|
|
|
const thinkingOverrideRaw = readStringParam(params, "thinking");
|
2026-01-06 08:41:45 +01:00
|
|
|
const cleanup =
|
2026-01-31 16:03:28 +09:00
|
|
|
params.cleanup === "keep" || params.cleanup === "delete" ? params.cleanup : "keep";
|
2026-01-17 03:57:59 +00:00
|
|
|
const requesterOrigin = normalizeDeliveryContext({
|
|
|
|
|
channel: opts?.agentChannel,
|
|
|
|
|
accountId: opts?.agentAccountId,
|
2026-01-20 17:22:07 +00:00
|
|
|
to: opts?.agentTo,
|
|
|
|
|
threadId: opts?.agentThreadId,
|
2026-01-17 03:57:59 +00:00
|
|
|
});
|
2026-01-07 16:14:25 +00:00
|
|
|
const runTimeoutSeconds = (() => {
|
|
|
|
|
const explicit =
|
2026-01-14 14:31:43 +00:00
|
|
|
typeof params.runTimeoutSeconds === "number" && Number.isFinite(params.runTimeoutSeconds)
|
2026-01-07 16:14:25 +00:00
|
|
|
? Math.max(0, Math.floor(params.runTimeoutSeconds))
|
|
|
|
|
: undefined;
|
2026-01-31 16:19:20 +09:00
|
|
|
if (explicit !== undefined) {
|
|
|
|
|
return explicit;
|
|
|
|
|
}
|
2026-01-07 16:14:25 +00:00
|
|
|
const legacy =
|
2026-01-14 14:31:43 +00:00
|
|
|
typeof params.timeoutSeconds === "number" && Number.isFinite(params.timeoutSeconds)
|
2026-01-07 16:14:25 +00:00
|
|
|
? Math.max(0, Math.floor(params.timeoutSeconds))
|
|
|
|
|
: undefined;
|
|
|
|
|
return legacy ?? 0;
|
|
|
|
|
})();
|
2026-01-07 04:48:20 +00:00
|
|
|
let modelWarning: string | undefined;
|
|
|
|
|
let modelApplied = false;
|
2026-01-06 08:41:45 +01:00
|
|
|
|
|
|
|
|
const cfg = loadConfig();
|
|
|
|
|
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
|
|
|
|
const requesterSessionKey = opts?.agentSessionKey;
|
2026-01-14 14:31:43 +00:00
|
|
|
if (typeof requesterSessionKey === "string" && isSubagentSessionKey(requesterSessionKey)) {
|
2026-01-06 08:40:21 +00:00
|
|
|
return jsonResult({
|
|
|
|
|
status: "forbidden",
|
|
|
|
|
error: "sessions_spawn is not allowed from sub-agent sessions",
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-06 08:41:45 +01:00
|
|
|
const requesterInternalKey = requesterSessionKey
|
|
|
|
|
? resolveInternalSessionKey({
|
|
|
|
|
key: requesterSessionKey,
|
|
|
|
|
alias,
|
|
|
|
|
mainKey,
|
|
|
|
|
})
|
|
|
|
|
: alias;
|
|
|
|
|
const requesterDisplayKey = resolveDisplaySessionKey({
|
|
|
|
|
key: requesterInternalKey,
|
|
|
|
|
alias,
|
|
|
|
|
mainKey,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-06 18:25:37 +00:00
|
|
|
const requesterAgentId = normalizeAgentId(
|
2026-01-25 06:10:48 -07:00
|
|
|
opts?.requesterAgentIdOverride ?? parseAgentSessionKey(requesterInternalKey)?.agentId,
|
2026-01-06 18:25:37 +00:00
|
|
|
);
|
2026-01-08 06:55:28 +00:00
|
|
|
const targetAgentId = requestedAgentId
|
|
|
|
|
? normalizeAgentId(requestedAgentId)
|
|
|
|
|
: requesterAgentId;
|
|
|
|
|
if (targetAgentId !== requesterAgentId) {
|
2026-01-14 14:31:43 +00:00
|
|
|
const allowAgents = resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? [];
|
2026-01-08 08:39:55 +01:00
|
|
|
const allowAny = allowAgents.some((value) => value.trim() === "*");
|
|
|
|
|
const normalizedTargetId = targetAgentId.toLowerCase();
|
2026-01-08 06:55:28 +00:00
|
|
|
const allowSet = new Set(
|
|
|
|
|
allowAgents
|
|
|
|
|
.filter((value) => value.trim() && value.trim() !== "*")
|
2026-01-08 08:39:55 +01:00
|
|
|
.map((value) => normalizeAgentId(value).toLowerCase()),
|
2026-01-08 06:55:28 +00:00
|
|
|
);
|
2026-01-08 08:39:55 +01:00
|
|
|
if (!allowAny && !allowSet.has(normalizedTargetId)) {
|
2026-01-08 06:55:28 +00:00
|
|
|
const allowedText = allowAny
|
|
|
|
|
? "*"
|
|
|
|
|
: allowSet.size > 0
|
|
|
|
|
? Array.from(allowSet).join(", ")
|
|
|
|
|
: "none";
|
|
|
|
|
return jsonResult({
|
|
|
|
|
status: "forbidden",
|
|
|
|
|
error: `agentId is not allowed for sessions_spawn (allowed: ${allowedText})`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
2026-01-24 05:49:23 +00:00
|
|
|
const spawnedByKey = requesterInternalKey;
|
2026-01-12 18:08:16 +00:00
|
|
|
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
|
|
|
|
|
const resolvedModel =
|
|
|
|
|
normalizeModelSelection(modelOverride) ??
|
|
|
|
|
normalizeModelSelection(targetAgentConfig?.subagents?.model) ??
|
|
|
|
|
normalizeModelSelection(cfg.agents?.defaults?.subagents?.model);
|
2026-02-02 12:14:17 -08:00
|
|
|
|
|
|
|
|
const resolvedThinkingDefaultRaw =
|
|
|
|
|
readStringParam(targetAgentConfig?.subagents ?? {}, "thinking") ??
|
|
|
|
|
readStringParam(cfg.agents?.defaults?.subagents ?? {}, "thinking");
|
|
|
|
|
|
2026-01-18 00:14:02 +00:00
|
|
|
let thinkingOverride: string | undefined;
|
2026-02-02 12:14:17 -08:00
|
|
|
const thinkingCandidateRaw = thinkingOverrideRaw || resolvedThinkingDefaultRaw;
|
|
|
|
|
if (thinkingCandidateRaw) {
|
|
|
|
|
const normalized = normalizeThinkLevel(thinkingCandidateRaw);
|
2026-01-18 00:14:02 +00:00
|
|
|
if (!normalized) {
|
|
|
|
|
const { provider, model } = splitModelRef(resolvedModel);
|
|
|
|
|
const hint = formatThinkingLevels(provider, model);
|
|
|
|
|
return jsonResult({
|
|
|
|
|
status: "error",
|
2026-02-02 12:14:17 -08:00
|
|
|
error: `Invalid thinking level "${thinkingCandidateRaw}". Use one of: ${hint}.`,
|
2026-01-18 00:14:02 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
thinkingOverride = normalized;
|
|
|
|
|
}
|
2026-01-12 18:08:16 +00:00
|
|
|
if (resolvedModel) {
|
2026-01-06 23:17:10 +00:00
|
|
|
try {
|
|
|
|
|
await callGateway({
|
|
|
|
|
method: "sessions.patch",
|
2026-01-12 18:08:16 +00:00
|
|
|
params: { key: childSessionKey, model: resolvedModel },
|
2026-01-06 23:17:10 +00:00
|
|
|
timeoutMs: 10_000,
|
|
|
|
|
});
|
2026-01-07 04:48:20 +00:00
|
|
|
modelApplied = true;
|
2026-01-06 23:17:10 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
const messageText =
|
2026-01-14 14:31:43 +00:00
|
|
|
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
2026-01-07 04:48:20 +00:00
|
|
|
const recoverable =
|
2026-01-14 14:31:43 +00:00
|
|
|
messageText.includes("invalid model") || messageText.includes("model not allowed");
|
2026-01-07 04:48:20 +00:00
|
|
|
if (!recoverable) {
|
|
|
|
|
return jsonResult({
|
|
|
|
|
status: "error",
|
|
|
|
|
error: messageText,
|
|
|
|
|
childSessionKey,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
modelWarning = messageText;
|
2026-01-06 23:17:10 +00:00
|
|
|
}
|
|
|
|
|
}
|
fix: cron scheduler reliability, store hardening, and UX improvements (#10776)
* refactor: update cron job wake mode and run mode handling
- Changed default wake mode from 'next-heartbeat' to 'now' in CronJobEditor and related CLI commands.
- Updated cron-tool tests to reflect changes in run mode, introducing 'due' and 'force' options.
- Enhanced cron-tool logic to handle new run modes and ensure compatibility with existing job structures.
- Added new tests for delivery plan consistency and job execution behavior under various conditions.
- Improved normalization functions to handle wake mode and session target casing.
This refactor aims to streamline cron job configurations and enhance the overall user experience with clearer defaults and improved functionality.
* test: enhance cron job functionality and UI
- Added tests to ensure the isolated agent correctly announces the final payload text when delivering messages via Telegram.
- Implemented a new function to pick the last deliverable payload from a list of delivery payloads.
- Enhanced the cron service to maintain legacy "every" jobs while minute cron jobs recompute schedules.
- Updated the cron store migration tests to verify the addition of anchorMs to legacy every schedules.
- Improved the UI for displaying cron job details, including job state and delivery information, with new styles and layout adjustments.
These changes aim to improve the reliability and user experience of the cron job system.
* test: enhance sessions thinking level handling
- Added tests to verify that the correct thinking levels are applied during session spawning.
- Updated the sessions-spawn-tool to include a new parameter for overriding thinking levels.
- Enhanced the UI to support additional thinking levels, including "xhigh" and "full", and improved the handling of current options in dropdowns.
These changes aim to improve the flexibility and accuracy of thinking level configurations in session management.
* feat: enhance session management and cron job functionality
- Introduced passthrough arguments in the test-parallel script to allow for flexible command-line options.
- Updated session handling to hide cron run alias session keys from the sessions list, improving clarity.
- Enhanced the cron service to accurately record job start times and durations, ensuring better tracking of job execution.
- Added tests to verify the correct behavior of the cron service under various conditions, including zero-delay timers.
These changes aim to improve the usability and reliability of session and cron job management.
* feat: implement job running state checks in cron service
- Added functionality to prevent manual job runs if a job is already in progress, enhancing job management.
- Updated the `isJobDue` function to include checks for running jobs, ensuring accurate scheduling.
- Enhanced the `run` function to return a specific reason when a job is already running.
- Introduced a new test case to verify the behavior of forced manual runs during active job execution.
These changes aim to improve the reliability and clarity of cron job execution and management.
* feat: add session ID and key to CronRunLogEntry model
- Introduced `sessionid` and `sessionkey` properties to the `CronRunLogEntry` struct for enhanced tracking of session-related information.
- Updated the initializer and Codable conformance to accommodate the new properties, ensuring proper serialization and deserialization.
These changes aim to improve the granularity of logging and session management within the cron job system.
* fix: improve session display name resolution
- Updated the `resolveSessionDisplayName` function to ensure that both label and displayName are trimmed and default to an empty string if not present.
- Enhanced the logic to prevent returning the key if it matches the label or displayName, improving clarity in session naming.
These changes aim to enhance the accuracy and usability of session display names in the UI.
* perf: skip cron store persist when idle timer tick produces no changes
recomputeNextRuns now returns a boolean indicating whether any job
state was mutated. The idle path in onTimer only persists when the
return value is true, eliminating unnecessary file writes every 60s
for far-future or idle schedules.
* fix: prep for merge - explicit delivery mode migration, docs + changelog (#10776) (thanks @tyler6204)
2026-02-06 18:03:03 -08:00
|
|
|
if (thinkingOverride !== undefined) {
|
|
|
|
|
try {
|
|
|
|
|
await callGateway({
|
|
|
|
|
method: "sessions.patch",
|
|
|
|
|
params: {
|
|
|
|
|
key: childSessionKey,
|
|
|
|
|
thinkingLevel: thinkingOverride === "off" ? null : thinkingOverride,
|
|
|
|
|
},
|
|
|
|
|
timeoutMs: 10_000,
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const messageText =
|
|
|
|
|
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
|
|
|
|
return jsonResult({
|
|
|
|
|
status: "error",
|
|
|
|
|
error: messageText,
|
|
|
|
|
childSessionKey,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-06 08:41:45 +01:00
|
|
|
const childSystemPrompt = buildSubagentSystemPrompt({
|
|
|
|
|
requesterSessionKey,
|
2026-01-17 03:57:59 +00:00
|
|
|
requesterOrigin,
|
2026-01-06 08:41:45 +01:00
|
|
|
childSessionKey,
|
|
|
|
|
label: label || undefined,
|
2026-01-10 00:01:16 +00:00
|
|
|
task,
|
2026-01-06 08:41:45 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const childIdem = crypto.randomUUID();
|
|
|
|
|
let childRunId: string = childIdem;
|
|
|
|
|
try {
|
2026-01-31 16:38:03 +09:00
|
|
|
const response = await callGateway<{ runId: string }>({
|
2026-01-06 08:41:45 +01:00
|
|
|
method: "agent",
|
|
|
|
|
params: {
|
|
|
|
|
message: task,
|
|
|
|
|
sessionKey: childSessionKey,
|
2026-01-17 03:57:59 +00:00
|
|
|
channel: requesterOrigin?.channel,
|
2026-02-02 17:05:55 +01:00
|
|
|
to: requesterOrigin?.to ?? undefined,
|
|
|
|
|
accountId: requesterOrigin?.accountId ?? undefined,
|
|
|
|
|
threadId:
|
|
|
|
|
requesterOrigin?.threadId != null ? String(requesterOrigin.threadId) : undefined,
|
2026-01-06 08:41:45 +01:00
|
|
|
idempotencyKey: childIdem,
|
|
|
|
|
deliver: false,
|
2026-01-09 23:00:23 +01:00
|
|
|
lane: AGENT_LANE_SUBAGENT,
|
2026-01-06 08:41:45 +01:00
|
|
|
extraSystemPrompt: childSystemPrompt,
|
2026-01-18 00:14:02 +00:00
|
|
|
thinking: thinkingOverride,
|
2026-01-07 16:14:25 +00:00
|
|
|
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
|
feat(sessions): expose label in sessions.list and support label lookup in sessions_send
- Add `label` field to session entries and expose it in `sessions.list`
- Display label column in the web UI sessions table
- Support `label` parameter in `sessions_send` for lookup by label instead of sessionKey
- `sessions.patch`: Accept and store `label` field
- `sessions.list`: Return `label` in session entries
- `sessions_spawn`: Pass label through to registry and announce flow
- `sessions_send`: Accept optional `label` param, lookup session by label if sessionKey not provided
- `agent` method: Accept `label` and `spawnedBy` params (stored in session entry)
- Add `label` column to sessions table in web UI
- Changed session store writes to merge with existing entry (`{ ...existing, ...new }`)
to preserve fields like `label` that might be set separately
We attempted to implement label persistence "properly" by passing the label
through the `agent` call and storing it during session initialization. However,
the auto-reply flow has multiple write points that overwrite the session entry,
and making all of them merge-aware proved unreliable.
The working solution patches the label in the `finally` block of
`runSubagentAnnounceFlow`, after all other session writes complete.
This is a workaround but robust - the patch happens at the very end,
just before potential cleanup.
A future refactor could make session writes consistently merge-based,
which would allow the cleaner approach of setting label at spawn time.
```typescript
// Spawn with label
sessions_spawn({ task: "...", label: "my-worker" })
// Later, find by label
sessions_send({ label: "my-worker", message: "continue..." })
// Or use sessions_list to see labels
sessions_list() // includes label field in response
```
2026-01-08 23:17:08 +00:00
|
|
|
label: label || undefined,
|
2026-01-24 05:49:23 +00:00
|
|
|
spawnedBy: spawnedByKey,
|
|
|
|
|
groupId: opts?.agentGroupId ?? undefined,
|
|
|
|
|
groupChannel: opts?.agentGroupChannel ?? undefined,
|
|
|
|
|
groupSpace: opts?.agentGroupSpace ?? undefined,
|
2026-01-06 08:41:45 +01:00
|
|
|
},
|
|
|
|
|
timeoutMs: 10_000,
|
2026-01-31 16:03:28 +09:00
|
|
|
});
|
2026-01-06 08:41:45 +01:00
|
|
|
if (typeof response?.runId === "string" && response.runId) {
|
|
|
|
|
childRunId = response.runId;
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const messageText =
|
2026-01-14 14:31:43 +00:00
|
|
|
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
2026-01-06 08:41:45 +01:00
|
|
|
return jsonResult({
|
|
|
|
|
status: "error",
|
|
|
|
|
error: messageText,
|
|
|
|
|
childSessionKey,
|
|
|
|
|
runId: childRunId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 06:53:01 +01:00
|
|
|
registerSubagentRun({
|
|
|
|
|
runId: childRunId,
|
|
|
|
|
childSessionKey,
|
|
|
|
|
requesterSessionKey: requesterInternalKey,
|
2026-01-17 03:57:59 +00:00
|
|
|
requesterOrigin,
|
2026-01-07 06:53:01 +01:00
|
|
|
requesterDisplayKey,
|
|
|
|
|
task,
|
|
|
|
|
cleanup,
|
feat(sessions): expose label in sessions.list and support label lookup in sessions_send
- Add `label` field to session entries and expose it in `sessions.list`
- Display label column in the web UI sessions table
- Support `label` parameter in `sessions_send` for lookup by label instead of sessionKey
- `sessions.patch`: Accept and store `label` field
- `sessions.list`: Return `label` in session entries
- `sessions_spawn`: Pass label through to registry and announce flow
- `sessions_send`: Accept optional `label` param, lookup session by label if sessionKey not provided
- `agent` method: Accept `label` and `spawnedBy` params (stored in session entry)
- Add `label` column to sessions table in web UI
- Changed session store writes to merge with existing entry (`{ ...existing, ...new }`)
to preserve fields like `label` that might be set separately
We attempted to implement label persistence "properly" by passing the label
through the `agent` call and storing it during session initialization. However,
the auto-reply flow has multiple write points that overwrite the session entry,
and making all of them merge-aware proved unreliable.
The working solution patches the label in the `finally` block of
`runSubagentAnnounceFlow`, after all other session writes complete.
This is a workaround but robust - the patch happens at the very end,
just before potential cleanup.
A future refactor could make session writes consistently merge-based,
which would allow the cleaner approach of setting label at spawn time.
```typescript
// Spawn with label
sessions_spawn({ task: "...", label: "my-worker" })
// Later, find by label
sessions_send({ label: "my-worker", message: "continue..." })
// Or use sessions_list to see labels
sessions_list() // includes label field in response
```
2026-01-08 23:17:08 +00:00
|
|
|
label: label || undefined,
|
2026-01-12 01:58:24 +00:00
|
|
|
runTimeoutSeconds,
|
2026-01-07 06:53:01 +01:00
|
|
|
});
|
|
|
|
|
|
2026-01-06 08:41:45 +01:00
|
|
|
return jsonResult({
|
2026-01-07 16:14:25 +00:00
|
|
|
status: "accepted",
|
2026-01-06 08:41:45 +01:00
|
|
|
childSessionKey,
|
|
|
|
|
runId: childRunId,
|
2026-01-12 18:08:16 +00:00
|
|
|
modelApplied: resolvedModel ? modelApplied : undefined,
|
2026-01-07 04:48:20 +00:00
|
|
|
warning: modelWarning,
|
2026-01-06 08:41:45 +01:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|