agent-relay-server 0.125.0 → 0.127.0
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/docs/openapi.json +1 -1
- package/package.json +9 -5
- package/public/assets/{MessageBubble-BYeYFteG.js → MessageBubble-D2cnxyTy.js} +2 -2
- package/public/assets/{MessageBubble-BYeYFteG.js.map → MessageBubble-D2cnxyTy.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-iAXvkZKW.js → PlanGraphPanel-Cnxt-x7R.js} +2 -2
- package/public/assets/{PlanGraphPanel-iAXvkZKW.js.map → PlanGraphPanel-Cnxt-x7R.js.map} +1 -1
- package/public/assets/{activity-Du9SRJt5.js → activity-BEm1BQB1.js} +2 -2
- package/public/assets/{activity-Du9SRJt5.js.map → activity-BEm1BQB1.js.map} +1 -1
- package/public/assets/{agent-profiles-DXDQnrlH.js → agent-profiles-C8V_oCiM.js} +2 -2
- package/public/assets/{agent-profiles-DXDQnrlH.js.map → agent-profiles-C8V_oCiM.js.map} +1 -1
- package/public/assets/{agent-type-icon-B3i0rndK.js → agent-type-icon-BmBR-9CH.js} +2 -2
- package/public/assets/{agent-type-icon-B3i0rndK.js.map → agent-type-icon-BmBR-9CH.js.map} +1 -1
- package/public/assets/{agents-CWOdKrxP.js → agents-D8mc9X1j.js} +2 -2
- package/public/assets/{agents-CWOdKrxP.js.map → agents-D8mc9X1j.js.map} +1 -1
- package/public/assets/{analytics-YcY59Hv9.js → analytics-BZtRycyL.js} +2 -2
- package/public/assets/{analytics-YcY59Hv9.js.map → analytics-BZtRycyL.js.map} +1 -1
- package/public/assets/{automation-CItTse7F.js → automation-BEhJZkNQ.js} +2 -2
- package/public/assets/{automation-CItTse7F.js.map → automation-BEhJZkNQ.js.map} +1 -1
- package/public/assets/{badge-XEru6wto.js → badge-BCvp7LK3.js} +2 -2
- package/public/assets/{badge-XEru6wto.js.map → badge-BCvp7LK3.js.map} +1 -1
- package/public/assets/{branch-state-badge-QjTktTYj.js → branch-state-badge-Cscjqf-E.js} +2 -2
- package/public/assets/{branch-state-badge-QjTktTYj.js.map → branch-state-badge-Cscjqf-E.js.map} +1 -1
- package/public/assets/{button-C3CQSr4p.js → button-DyHjsHY3.js} +2 -2
- package/public/assets/{button-C3CQSr4p.js.map → button-DyHjsHY3.js.map} +1 -1
- package/public/assets/{card-C5XTIPvq.js → card-BoqYxpVW.js} +2 -2
- package/public/assets/{card-C5XTIPvq.js.map → card-BoqYxpVW.js.map} +1 -1
- package/public/assets/{channels-BSFTsbhT.js → channels-DAtOnA6o.js} +2 -2
- package/public/assets/{channels-BSFTsbhT.js.map → channels-DAtOnA6o.js.map} +1 -1
- package/public/assets/chat-BAH9bEPx.js +2 -0
- package/public/assets/chat-BAH9bEPx.js.map +1 -0
- package/public/assets/{connectors-C7d5dlYz.js → connectors-B2grC-hl.js} +2 -2
- package/public/assets/{connectors-C7d5dlYz.js.map → connectors-B2grC-hl.js.map} +1 -1
- package/public/assets/{copy-button-Bil2CjzT.js → copy-button-C67FUG3E.js} +2 -2
- package/public/assets/{copy-button-Bil2CjzT.js.map → copy-button-C67FUG3E.js.map} +1 -1
- package/public/assets/{dist-V3wu3BHw.js → dist-BD4Xc13v.js} +2 -2
- package/public/assets/{dist-V3wu3BHw.js.map → dist-BD4Xc13v.js.map} +1 -1
- package/public/assets/{dist-C5z05Vkz.js → dist-BFDfbfNC.js} +2 -2
- package/public/assets/{dist-C5z05Vkz.js.map → dist-BFDfbfNC.js.map} +1 -1
- package/public/assets/{dist-CjyfQThq.js → dist-BJKxqIQd.js} +2 -2
- package/public/assets/{dist-CjyfQThq.js.map → dist-BJKxqIQd.js.map} +1 -1
- package/public/assets/{dist-B8tD9ZcR.js → dist-BNB3fTO-.js} +2 -2
- package/public/assets/{dist-B8tD9ZcR.js.map → dist-BNB3fTO-.js.map} +1 -1
- package/public/assets/{dist-CFtFIhu3.js → dist-CHKra2JV.js} +2 -2
- package/public/assets/{dist-CFtFIhu3.js.map → dist-CHKra2JV.js.map} +1 -1
- package/public/assets/{dist-r8wyUTkK.js → dist-CQMQ5iku.js} +2 -2
- package/public/assets/{dist-r8wyUTkK.js.map → dist-CQMQ5iku.js.map} +1 -1
- package/public/assets/{dist-DKmTXfpP.js → dist-C_VOBSaK.js} +2 -2
- package/public/assets/{dist-DKmTXfpP.js.map → dist-C_VOBSaK.js.map} +1 -1
- package/public/assets/{dist-CGaHpxnN.js → dist-D5w04z0l.js} +2 -2
- package/public/assets/{dist-CGaHpxnN.js.map → dist-D5w04z0l.js.map} +1 -1
- package/public/assets/{dist-4VwhnLtj.js → dist-DG75bvPp.js} +2 -2
- package/public/assets/{dist-4VwhnLtj.js.map → dist-DG75bvPp.js.map} +1 -1
- package/public/assets/{dist-CrUl2aQp.js → dist-G8YBD-wp.js} +2 -2
- package/public/assets/{dist-CrUl2aQp.js.map → dist-G8YBD-wp.js.map} +1 -1
- package/public/assets/{dist-BJaV2BD4.js → dist-f6MW74eX.js} +2 -2
- package/public/assets/{dist-BJaV2BD4.js.map → dist-f6MW74eX.js.map} +1 -1
- package/public/assets/{es2015-5DTJe35n.js → es2015-a6fysEVz.js} +2 -2
- package/public/assets/{es2015-5DTJe35n.js.map → es2015-a6fysEVz.js.map} +1 -1
- package/public/assets/formatted-body-Rv9RDPsQ.js +3 -0
- package/public/assets/{formatted-body-BuLfVMeD.js.map → formatted-body-Rv9RDPsQ.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DO-8giS6.js → formatted-body-impl-XFU4tPeT.js} +2 -2
- package/public/assets/{formatted-body-impl-DO-8giS6.js.map → formatted-body-impl-XFU4tPeT.js.map} +1 -1
- package/public/assets/{index-De1tR7J4.js → index-Bhy_jnX4.js} +6 -6
- package/public/assets/{index-De1tR7J4.js.map → index-Bhy_jnX4.js.map} +1 -1
- package/public/assets/index-DHzXsQot.css +2 -0
- package/public/assets/{input-CXMbJ848.js → input-Rc4oWv4q.js} +2 -2
- package/public/assets/{input-CXMbJ848.js.map → input-Rc4oWv4q.js.map} +1 -1
- package/public/assets/{insights-Ci3VuA5Y.js → insights-DpXPAEwH.js} +2 -2
- package/public/assets/{insights-Ci3VuA5Y.js.map → insights-DpXPAEwH.js.map} +1 -1
- package/public/assets/{integrations-BboxWQQD.js → integrations-9c6IdRU4.js} +2 -2
- package/public/assets/{integrations-BboxWQQD.js.map → integrations-9c6IdRU4.js.map} +1 -1
- package/public/assets/{lib-BcUW1l-W.js → lib-BZ6WPg2t.js} +2 -2
- package/public/assets/{lib-BcUW1l-W.js.map → lib-BZ6WPg2t.js.map} +1 -1
- package/public/assets/lucide-react-NXHPIMsX.js +10 -0
- package/public/assets/lucide-react-NXHPIMsX.js.map +1 -0
- package/public/assets/{maintenance-CO1miJIJ.js → maintenance-BrkqfGwx.js} +2 -2
- package/public/assets/{maintenance-CO1miJIJ.js.map → maintenance-BrkqfGwx.js.map} +1 -1
- package/public/assets/{managed-agents-r4oYK3Fm.js → managed-agents-CLANxOAN.js} +2 -2
- package/public/assets/{managed-agents-r4oYK3Fm.js.map → managed-agents-CLANxOAN.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-5CCM9iyL.js → markdown-preview-impl-CcZFkjn-.js} +2 -2
- package/public/assets/{markdown-preview-impl-5CCM9iyL.js.map → markdown-preview-impl-CcZFkjn-.js.map} +1 -1
- package/public/assets/{memory-BJJS07W-.js → memory-BoQw8q-i.js} +2 -2
- package/public/assets/{memory-BJJS07W-.js.map → memory-BoQw8q-i.js.map} +1 -1
- package/public/assets/{messages-RT_dfEsL.js → messages-DfKNuph2.js} +2 -2
- package/public/assets/{messages-RT_dfEsL.js.map → messages-DfKNuph2.js.map} +1 -1
- package/public/assets/{orchestrators-8hyl8hW1.js → orchestrators-sQh8e7-V.js} +2 -2
- package/public/assets/{orchestrators-8hyl8hW1.js.map → orchestrators-sQh8e7-V.js.map} +1 -1
- package/public/assets/{overview-CUJ9QmW_.js → overview-B0z8ZNtp.js} +2 -2
- package/public/assets/{overview-CUJ9QmW_.js.map → overview-B0z8ZNtp.js.map} +1 -1
- package/public/assets/{pairs-CzkzMGZq.js → pairs-B3FhGmn-.js} +2 -2
- package/public/assets/{pairs-CzkzMGZq.js.map → pairs-B3FhGmn-.js.map} +1 -1
- package/public/assets/{projects-CAuLWBU5.js → projects-iDkxAGBW.js} +2 -2
- package/public/assets/{projects-CAuLWBU5.js.map → projects-iDkxAGBW.js.map} +1 -1
- package/public/assets/{prompt-settings-BnB90R7F.js → prompt-settings-CCj_s008.js} +2 -2
- package/public/assets/{prompt-settings-BnB90R7F.js.map → prompt-settings-CCj_s008.js.map} +1 -1
- package/public/assets/{react-dom-BQICbUuF.js → react-dom-8yaD9iJG.js} +2 -2
- package/public/assets/{react-dom-BQICbUuF.js.map → react-dom-8yaD9iJG.js.map} +1 -1
- package/public/assets/{registry-B8TInqru.js → registry-mD_y06Ru.js} +2 -2
- package/public/assets/{registry-B8TInqru.js.map → registry-mD_y06Ru.js.map} +1 -1
- package/public/assets/{security-DG_VVZUW.js → security-CTSA9niL.js} +2 -2
- package/public/assets/{security-DG_VVZUW.js.map → security-CTSA9niL.js.map} +1 -1
- package/public/assets/{select-BJ8G38_V.js → select-gEQEHCIf.js} +2 -2
- package/public/assets/{select-BJ8G38_V.js.map → select-gEQEHCIf.js.map} +1 -1
- package/public/assets/{settings-CK_T9z5w.js → settings-ByAR-TYj.js} +2 -2
- package/public/assets/{settings-CK_T9z5w.js.map → settings-ByAR-TYj.js.map} +1 -1
- package/public/assets/store-D-Frk_EC.js +9 -0
- package/public/assets/store-D-Frk_EC.js.map +1 -0
- package/public/assets/{task-chip-BHM-e3I8.js → task-chip-CeujtXYT.js} +2 -2
- package/public/assets/{task-chip-BHM-e3I8.js.map → task-chip-CeujtXYT.js.map} +1 -1
- package/public/assets/{tasks-Briui9Mp.js → tasks-CZyJX881.js} +2 -2
- package/public/assets/{tasks-Briui9Mp.js.map → tasks-CZyJX881.js.map} +1 -1
- package/public/assets/{teams-Ck4EVV3R.js → teams-BrL4sEL7.js} +2 -2
- package/public/assets/{teams-Ck4EVV3R.js.map → teams-BrL4sEL7.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-66ZjVN1v.js → terminal-viewer-impl-LjIph8ql.js} +2 -2
- package/public/assets/{terminal-viewer-impl-66ZjVN1v.js.map → terminal-viewer-impl-LjIph8ql.js.map} +1 -1
- package/public/assets/types-BqeSkWrv.js.map +1 -1
- package/public/assets/{use-keyboard-viewport-CAa7IQTm.js → use-keyboard-viewport-cGEmrGE7.js} +2 -2
- package/public/assets/{use-keyboard-viewport-CAa7IQTm.js.map → use-keyboard-viewport-cGEmrGE7.js.map} +1 -1
- package/public/assets/{user-avatar-BOy9o2xK.js → user-avatar-qbo-B3dQ.js} +2 -2
- package/public/assets/{user-avatar-BOy9o2xK.js.map → user-avatar-qbo-B3dQ.js.map} +1 -1
- package/public/assets/{work-queue-CnghFRG2.js → work-queue-CSu5kRsw.js} +2 -2
- package/public/assets/{work-queue-CnghFRG2.js.map → work-queue-CSu5kRsw.js.map} +1 -1
- package/public/assets/{workspaces-BzmQnzSm.js → workspaces-CoDz-iZd.js} +2 -2
- package/public/assets/{workspaces-BzmQnzSm.js.map → workspaces-CoDz-iZd.js.map} +1 -1
- package/public/index.html +24 -24
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +2 -2
- package/runner/src/adapters/codex-agent-message-capture.ts +38 -8
- package/runner/src/relay-mcp-proxy.ts +1 -3
- package/runner/src/runner-core.ts +33 -13
- package/runner/src/runner-helpers.ts +31 -0
- package/src/db/index.ts +1 -0
- package/src/db/messages.ts +38 -2
- package/src/db/task-deliverables.ts +206 -0
- package/src/db/task-entity.ts +4 -8
- package/src/mcp/index.ts +4 -4
- package/src/mcp/tools-workspace.ts +3 -36
- package/src/mcp-task-deliver-tools.ts +143 -0
- package/src/notification-rules.ts +1 -0
- package/src/notification-types.ts +2 -0
- package/src/prompt-resolver.ts +6 -6
- package/src/routes/agent-sessions.ts +10 -7
- package/src/routes/orchestrator.ts +5 -2
- package/src/routes/settings.ts +31 -2
- package/src/services/restart-instructions.ts +36 -0
- package/src/services/task-delivery.ts +275 -0
- package/src/settings/registry.ts +14 -3
- package/src/workspace-phase.ts +5 -5
- package/public/assets/chat-CRzFSFWe.js +0 -2
- package/public/assets/chat-CRzFSFWe.js.map +0 -1
- package/public/assets/formatted-body-BuLfVMeD.js +0 -3
- package/public/assets/index-C9X_0QBt.css +0 -2
- package/public/assets/lucide-react-CoU_Xts2.js +0 -10
- package/public/assets/lucide-react-CoU_Xts2.js.map +0 -1
- package/public/assets/store-Dl9Z8YUS.js +0 -9
- package/public/assets/store-Dl9Z8YUS.js.map +0 -1
|
@@ -4,7 +4,7 @@ import { CONTRACT_VERSIONS, parseRuntimeCapabilities, parseRuntimeContracts, par
|
|
|
4
4
|
import { notifyAgentOffline } from "../agent-lifecycle-events";
|
|
5
5
|
import { diagnoseAgentDeath } from "../agent-death-diagnosis";
|
|
6
6
|
import { VERSION } from "../config";
|
|
7
|
-
import { ValidationError, deleteOrchestrator, getOrchestrator, listOrchestrators, orchestratorHeartbeat, recordAgentExitDiagnostics, setOrchestratorUpgradeState, updateManagedAgents, upsertOrchestrator } from "../db";
|
|
7
|
+
import { ValidationError, deleteOrchestrator, getAgent, getOrchestrator, listOrchestrators, orchestratorHeartbeat, recordAgentExitDiagnostics, setOrchestratorUpgradeState, updateManagedAgents, upsertOrchestrator } from "../db";
|
|
8
8
|
import { auditEvent, authAuditMetadata, authorizeRoute, cleanJsonArray, cleanSafeNumber, dashboardAttribution, emitCommand, error, isRootCredentialRequest, json, parseBody, spawnRequestId, type Handler } from "./_shared";
|
|
9
9
|
import { cleanMeta, cleanString, cleanStringArray, cleanWorkspaceMetadata, optionalEnum } from "../validation";
|
|
10
10
|
import { createCommand, updateCommand } from "../commands-db";
|
|
@@ -13,6 +13,7 @@ import { getLifecycleManager } from "../lifecycle-manager";
|
|
|
13
13
|
import { issueOrchestratorRuntimeToken } from "../runtime-tokens";
|
|
14
14
|
import { isOrchestratorRegistrationAuthorized } from "../security";
|
|
15
15
|
import { type ManagedAgent, type ManagedSessionExitDiagnostics, type OrchestratorRuntimeInput, type RegisterOrchestratorInput, type SpawnApprovalMode, type SpawnProvider } from "../types";
|
|
16
|
+
import { restartSpawnParamsForAgent } from "./agent-sessions";
|
|
16
17
|
|
|
17
18
|
function cleanProtocolVersion(value: unknown): number | undefined {
|
|
18
19
|
if (value === undefined) return undefined;
|
|
@@ -533,13 +534,15 @@ export const postOrchestratorAction: Handler = async (req, params) => {
|
|
|
533
534
|
resource: { orchestratorId: orch.id, agentId, policyName, spawnRequestId },
|
|
534
535
|
});
|
|
535
536
|
if (denied) return denied;
|
|
537
|
+
const agent = action === "restart" && agentId ? getAgent(agentId) : undefined;
|
|
538
|
+
const restartSpawn = agent ? restartSpawnParamsForAgent(agent, orch, policyName, spawnRequestId) : undefined;
|
|
536
539
|
|
|
537
540
|
const command = createCommand({
|
|
538
541
|
type: action === "restart" ? "agent.restart" : "agent.shutdown",
|
|
539
542
|
source: "system",
|
|
540
543
|
target: orch.agentId,
|
|
541
544
|
correlationId: spawnRequestId,
|
|
542
|
-
params: { action, agentId, policyName, spawnRequestId, sessionName, tmuxSession, reason, requestedBy: "dashboard", requestedAt: Date.now(), orchestratorId: orch.id },
|
|
545
|
+
params: { action, agentId, policyName, spawnRequestId, sessionName, tmuxSession, reason, ...(restartSpawn ? { restartSpawn } : {}), requestedBy: "dashboard", requestedAt: Date.now(), orchestratorId: orch.id },
|
|
543
546
|
});
|
|
544
547
|
emitCommand(command);
|
|
545
548
|
auditEvent({
|
package/src/routes/settings.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { isRequestAuthorizedFor } from "../security";
|
|
|
10
10
|
import { cleanString } from "../validation";
|
|
11
11
|
import { emitConfigChanged } from "../sse";
|
|
12
12
|
import { error, json, normalizeConfigPathParam, parseBody, requestUserId, type Handler } from "./_shared";
|
|
13
|
-
import { DEFAULT_USER_ID, isRecord, type ConfigEntry, type SettingDescriptor, type SettingEntry } from "agent-relay-sdk";
|
|
13
|
+
import { DEFAULT_USER_ID, isRecord, type ConfigEntry, type JSONSchema, type SettingDescriptor, type SettingEntry } from "agent-relay-sdk";
|
|
14
14
|
|
|
15
15
|
type SettingEnvelope = SettingEntry;
|
|
16
16
|
|
|
@@ -91,7 +91,10 @@ async function putSetting(req: Request, namespaceRaw: string | undefined, keyRaw
|
|
|
91
91
|
if (!validation.valid) return json({ error: "invalid setting value", details: validation.errors }, 400);
|
|
92
92
|
if (descriptor.tier === "user") {
|
|
93
93
|
const userId = requestUserId(req);
|
|
94
|
-
const
|
|
94
|
+
const valueToStore = descriptor.mergeMapKeys && isRecord(parsed.body.value)
|
|
95
|
+
? mergeMapKeysIntoExisting(getUserSetting(userId, namespace, key)?.value, parsed.body.value, descriptor.schema)
|
|
96
|
+
: parsed.body.value;
|
|
97
|
+
const userEntry = setUserSetting(userId, namespace, key, valueToStore, updatedBy);
|
|
95
98
|
emitConfigChanged(userEntry.namespace, userEntry.key, userEntry.version);
|
|
96
99
|
return json(settingEnvelope(req, descriptor, userEntry), userEntry.version === 1 ? 201 : 200);
|
|
97
100
|
}
|
|
@@ -156,3 +159,29 @@ function mergeSettingDefaults(defaults: unknown, value: unknown): unknown {
|
|
|
156
159
|
}
|
|
157
160
|
return merged;
|
|
158
161
|
}
|
|
162
|
+
|
|
163
|
+
// A `mergeMapKeys` descriptor (#1124) is a dictionary keyed by caller-chosen ids (e.g. one row
|
|
164
|
+
// per conversation). A PUT only ever knows its own device's changed keys, never every other
|
|
165
|
+
// device's, so overwriting the whole stored value like a normal setting would erase them — this
|
|
166
|
+
// merges just the incoming keys on top of whatever is already stored instead. The merged result
|
|
167
|
+
// is then capped at the schema's `maxProperties` (when set) so per-key merges from unboundedly
|
|
168
|
+
// many devices can't grow the row forever; the entry with the OLDEST `updatedAt` is evicted first
|
|
169
|
+
// (missing/non-numeric `updatedAt`, e.g. a pre-#1124 row, sorts as oldest: 0).
|
|
170
|
+
function mergeMapKeysIntoExisting(existing: unknown, incoming: Record<string, unknown>, schema: JSONSchema): Record<string, unknown> {
|
|
171
|
+
const merged: Record<string, unknown> = isRecord(existing) ? { ...existing } : {};
|
|
172
|
+
for (const [key, item] of Object.entries(incoming)) merged[key] = item;
|
|
173
|
+
|
|
174
|
+
const cap = typeof schema.maxProperties === "number" ? schema.maxProperties : undefined;
|
|
175
|
+
const entries = Object.entries(merged);
|
|
176
|
+
if (cap !== undefined && entries.length > cap) {
|
|
177
|
+
entries.sort((a, b) => entryUpdatedAt(a[1]) - entryUpdatedAt(b[1]));
|
|
178
|
+
const overflow = entries.length - cap;
|
|
179
|
+
for (let i = 0; i < overflow; i += 1) delete merged[entries[i]![0]];
|
|
180
|
+
}
|
|
181
|
+
return merged;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function entryUpdatedAt(value: unknown): number {
|
|
185
|
+
if (isRecord(value) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt)) return value.updatedAt;
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isSpawnProvider } from "agent-relay-sdk";
|
|
2
|
+
import { getAgentProfile } from "../config-store";
|
|
3
|
+
import { readSpawnCallerSystemPromptAppend, resolveAgentOwnership, withResolvedProvisioning } from "../db";
|
|
4
|
+
import { composeProfileSpawnInstructions } from "../project-instructions";
|
|
5
|
+
import { applyDefaultSharedMcp } from "../shared-mcp";
|
|
6
|
+
import type { AgentCard } from "../types";
|
|
7
|
+
|
|
8
|
+
function metaString(meta: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
9
|
+
const value = meta?.[key];
|
|
10
|
+
return typeof value === "string" ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function restartInstructionParamsForAgent(
|
|
14
|
+
agent: AgentCard,
|
|
15
|
+
opts: { spawnRequestId?: string; cwd?: string } = {},
|
|
16
|
+
): Record<string, unknown> | undefined {
|
|
17
|
+
const provider = metaString(agent.meta, "provider");
|
|
18
|
+
if (!isSpawnProvider(provider)) return undefined;
|
|
19
|
+
const cwd = opts.cwd ?? metaString(agent.meta, "cwd");
|
|
20
|
+
const profileName = metaString(agent.meta, "profile");
|
|
21
|
+
const resolvedProfile = withResolvedProvisioning(profileName ? getAgentProfile(profileName)?.value : undefined, provider);
|
|
22
|
+
const agentProfile = resolvedProfile ? applyDefaultSharedMcp(resolvedProfile) : undefined;
|
|
23
|
+
const callerAppend = readSpawnCallerSystemPromptAppend(opts.spawnRequestId);
|
|
24
|
+
const composed = composeProfileSpawnInstructions({
|
|
25
|
+
...(cwd ? { cwd } : {}),
|
|
26
|
+
operatorId: resolveAgentOwnership(agent.id).ownerUserId,
|
|
27
|
+
...(profileName ? { profileName } : {}),
|
|
28
|
+
...(agentProfile ? { profile: agentProfile } : {}),
|
|
29
|
+
...(callerAppend ? { callerSystemPromptAppend: callerAppend } : {}),
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
...(composed.systemPromptAppend ? { systemPromptAppend: composed.systemPromptAppend } : {}),
|
|
33
|
+
...(callerAppend ? { callerSystemPromptAppend: callerAppend } : {}),
|
|
34
|
+
...(composed.relayInjectionEvents.length ? { relayInjectionEvents: composed.relayInjectionEvents } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTaskDetail,
|
|
3
|
+
getWorkspace,
|
|
4
|
+
insertTaskEvent,
|
|
5
|
+
listTaskEvents,
|
|
6
|
+
ownedIsolatedWorkspace,
|
|
7
|
+
recordGate,
|
|
8
|
+
recordTaskDeliverable,
|
|
9
|
+
setTaskStatus,
|
|
10
|
+
ValidationError,
|
|
11
|
+
} from "../db";
|
|
12
|
+
import { emitRelayEvent } from "../events";
|
|
13
|
+
import { routeNotification } from "../notification-router";
|
|
14
|
+
import { emitTaskChanged, emitNewMessages } from "../sse";
|
|
15
|
+
import { agentBoundTaskIds, hasTaskReadAdminScope } from "../task-authz";
|
|
16
|
+
import { applyWorkspaceAction } from "../workspace-actions";
|
|
17
|
+
import { emitTaskSemanticNotification } from "./task-semantic-events";
|
|
18
|
+
import { ServiceAuthError } from "./errors";
|
|
19
|
+
import { resolveCoordinatorAgentId } from "../steward-identity";
|
|
20
|
+
import { markAgentWorkFinal } from "./work-finality";
|
|
21
|
+
import type { AuthContext } from "./auth-context";
|
|
22
|
+
import type { Command, TaskBlocker, TaskDeliverableEventMetadata, TaskDeliveryGateReports, TaskDeliveryResult, TaskGateStatus, TaskDetail, WorkspaceRecord } from "../types";
|
|
23
|
+
|
|
24
|
+
const REQUIRED_COMPLETE_GATES = ["typecheck", "test"] as const;
|
|
25
|
+
|
|
26
|
+
export interface DeliverTaskInput extends TaskDeliveryResult {
|
|
27
|
+
taskId?: number;
|
|
28
|
+
workspaceId?: string;
|
|
29
|
+
idempotencyKey?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface GateWrite {
|
|
33
|
+
gate: string;
|
|
34
|
+
status: TaskGateStatus;
|
|
35
|
+
detail?: string;
|
|
36
|
+
count?: number;
|
|
37
|
+
runId?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function callerLabel(ctx: AuthContext): string {
|
|
41
|
+
return ctx.callerAgentId ?? ctx.actor.id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function taskIdFrom(value: unknown): number | undefined {
|
|
45
|
+
if (value === undefined || value === null) return undefined;
|
|
46
|
+
const n = typeof value === "string" ? Number(value) : value;
|
|
47
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) throw new ValidationError("taskId must be a positive integer");
|
|
48
|
+
return n;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveTaskId(ctx: AuthContext, explicit: unknown): number {
|
|
52
|
+
const taskId = taskIdFrom(explicit);
|
|
53
|
+
const caller = ctx.callerAgentId;
|
|
54
|
+
const bound = caller ? agentBoundTaskIds(caller) : new Set<number>();
|
|
55
|
+
const admin = hasTaskReadAdminScope(ctx.scopes);
|
|
56
|
+
if (taskId !== undefined) {
|
|
57
|
+
if (admin || bound.has(taskId)) return taskId;
|
|
58
|
+
throw new ServiceAuthError("taskId is not bound to the caller");
|
|
59
|
+
}
|
|
60
|
+
if (bound.size === 1) return [...bound][0]!;
|
|
61
|
+
if (bound.size === 0) throw new ServiceAuthError("caller is not bound to a Task");
|
|
62
|
+
throw new ValidationError("caller is bound to multiple Tasks; pass taskId explicitly");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveWorkspace(ctx: AuthContext, input: { workspaceId?: string }, taskId: number): WorkspaceRecord | undefined {
|
|
66
|
+
const caller = ctx.callerAgentId;
|
|
67
|
+
const explicit = input.workspaceId?.trim();
|
|
68
|
+
const ws = explicit ? getWorkspace(explicit) : caller ? ownedIsolatedWorkspace(caller) : undefined;
|
|
69
|
+
if (!ws) return undefined;
|
|
70
|
+
if (!hasTaskReadAdminScope(ctx.scopes) && (!caller || ws.ownerAgentId !== caller)) {
|
|
71
|
+
throw new ServiceAuthError(`workspace ${ws.id} is not owned by the caller`);
|
|
72
|
+
}
|
|
73
|
+
const task = getTaskDetail(taskId);
|
|
74
|
+
if (!task) throw new ValidationError("task not found");
|
|
75
|
+
if (task.branch && ws.branch && task.branch !== ws.branch) {
|
|
76
|
+
throw new ValidationError(`task ${taskId} is bound to branch ${task.branch}, not workspace branch ${ws.branch}`);
|
|
77
|
+
}
|
|
78
|
+
return ws;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function gateWrites(gates: TaskDeliveryGateReports | undefined, deliveryKey: string): GateWrite[] {
|
|
82
|
+
const writes: GateWrite[] = [];
|
|
83
|
+
if (gates?.typecheck) writes.push({ gate: "typecheck", ...gatePayload(gates.typecheck, `${deliveryKey}:typecheck`) });
|
|
84
|
+
if (gates?.test) writes.push({ gate: "test", ...gatePayload(gates.test, `${deliveryKey}:test`) });
|
|
85
|
+
for (const gate of gates?.other ?? []) {
|
|
86
|
+
writes.push({ gate: gate.name, ...gatePayload(gate, `${deliveryKey}:other:${gate.name}`) });
|
|
87
|
+
}
|
|
88
|
+
return writes;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function gatePayload(input: { status: TaskGateStatus; detail?: string; count?: number; runId?: string }, fallbackRunId: string): Omit<GateWrite, "gate"> {
|
|
92
|
+
return {
|
|
93
|
+
status: input.status,
|
|
94
|
+
...(input.detail ? { detail: input.detail } : {}),
|
|
95
|
+
...(typeof input.count === "number" ? { count: input.count } : {}),
|
|
96
|
+
runId: input.runId ?? fallbackRunId,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizedDoneWhen(task: TaskDetail): string {
|
|
101
|
+
return (task.doneWhen ?? "").toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function completeDeliveryLands(task: TaskDetail): boolean {
|
|
105
|
+
const dispatchType = task.dispatch?.type;
|
|
106
|
+
if (dispatchType === "review" || dispatchType === "research" || dispatchType === "ops") return false;
|
|
107
|
+
const acceptance = normalizedDoneWhen(task);
|
|
108
|
+
if (/\b(no[-\s]?branch|review(?:ed)?|verdict|report[-\s]?only|none)\b/.test(acceptance)) return false;
|
|
109
|
+
if (/\b(merged?|landed(?:-on-main)?|live[-\s]?verified|deployed|agent[-\s]?exit|reaped)\b/.test(acceptance)) return true;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cleanReason(value: unknown, disposition: "blocked" | "failed"): string {
|
|
114
|
+
if (typeof value !== "string" || !value.trim()) throw new ValidationError(`${disposition} delivery requires reason`);
|
|
115
|
+
const cleaned = value.trim();
|
|
116
|
+
if (cleaned.length > 64_000) throw new ValidationError("reason must be 64000 characters or fewer");
|
|
117
|
+
return cleaned;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cleanBlockedBy(value: unknown): TaskBlocker[] {
|
|
121
|
+
if (!Array.isArray(value) || value.length === 0) throw new ValidationError("blocked delivery requires blockedBy");
|
|
122
|
+
return value as TaskBlocker[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function completeGateProblem(gates: TaskDeliveryGateReports | undefined): string | undefined {
|
|
126
|
+
if (!gates) return "missing-gate-block";
|
|
127
|
+
const writes = gateWrites(gates, "check");
|
|
128
|
+
const byName = new Map(writes.map((gate) => [gate.gate, gate.status]));
|
|
129
|
+
const missing = REQUIRED_COMPLETE_GATES.filter((gate) => !byName.has(gate));
|
|
130
|
+
if (missing.length > 0) return `missing-gates:${missing.join("+")}`;
|
|
131
|
+
const failed = writes.filter((gate) => gate.status !== "pass").map((gate) => gate.gate);
|
|
132
|
+
if (failed.length > 0) return `failed-gates:${failed.join("+")}`;
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function deliveryEvent(taskId: number, idempotencyKey: string | undefined): { id: number; metadata: TaskDeliverableEventMetadata } | undefined {
|
|
137
|
+
const events = listTaskEvents(taskId).filter((event) => event.type === "deliverable").reverse();
|
|
138
|
+
const event = idempotencyKey
|
|
139
|
+
? events.find((item) => item.metadata.idempotencyKey === idempotencyKey)
|
|
140
|
+
: events[0];
|
|
141
|
+
if (!event) return undefined;
|
|
142
|
+
return { id: event.id, metadata: event.metadata as unknown as TaskDeliverableEventMetadata };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function notifyDelivery(taskId: number, eventId: number, metadata: TaskDeliverableEventMetadata): void {
|
|
146
|
+
const task = getTaskDetail(taskId);
|
|
147
|
+
if (!task) return;
|
|
148
|
+
emitRelayEvent({
|
|
149
|
+
type: "task.deliverable",
|
|
150
|
+
source: metadata.actor,
|
|
151
|
+
subject: String(taskId),
|
|
152
|
+
data: { taskId, eventId, disposition: metadata.disposition, summary: metadata.summary, workspaceId: metadata.workspaceId },
|
|
153
|
+
});
|
|
154
|
+
const coordinator = resolveCoordinatorAgentId(task.ownerAgentId ?? metadata.actor);
|
|
155
|
+
const recipients = [...new Set([coordinator, task.createdBy].filter((item): item is string => Boolean(item && item.trim())))];
|
|
156
|
+
const delivered = routeNotification({
|
|
157
|
+
type: "task.deliverable",
|
|
158
|
+
scope: {
|
|
159
|
+
taskId: String(taskId),
|
|
160
|
+
transition: "deliverable",
|
|
161
|
+
...(task.ownerAgentId ? { agentId: task.ownerAgentId, author: task.ownerAgentId } : {}),
|
|
162
|
+
...(task.createdBy ? { parent: task.createdBy } : {}),
|
|
163
|
+
...(task.teamId ? { teamId: task.teamId } : {}),
|
|
164
|
+
...(metadata.workspaceId ? { workspaceId: metadata.workspaceId } : {}),
|
|
165
|
+
},
|
|
166
|
+
recipients,
|
|
167
|
+
ttlMs: null,
|
|
168
|
+
message: {
|
|
169
|
+
subject: `Task T${taskId} delivery: ${metadata.disposition}`,
|
|
170
|
+
body: `Task T${taskId} delivery (${metadata.disposition}): ${metadata.summary}`,
|
|
171
|
+
payload: {
|
|
172
|
+
kind: "task.deliverable",
|
|
173
|
+
notification: "task-deliverable",
|
|
174
|
+
taskId,
|
|
175
|
+
eventId,
|
|
176
|
+
disposition: metadata.disposition,
|
|
177
|
+
summary: metadata.summary,
|
|
178
|
+
gates: metadata.gates,
|
|
179
|
+
risks: metadata.risks,
|
|
180
|
+
workspaceId: metadata.workspaceId,
|
|
181
|
+
},
|
|
182
|
+
idempotencyKey: metadata.idempotencyKey ? `task:${taskId}:deliverable:${metadata.idempotencyKey}` : `task:${taskId}:deliverable:event:${eventId}`,
|
|
183
|
+
replyExpected: false,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (delivered.length > 0) emitNewMessages(delivered);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function emitCommand(command: Command): void {
|
|
190
|
+
emitRelayEvent({
|
|
191
|
+
type: command.status === "pending" ? "command.requested" : `command.${command.status}`,
|
|
192
|
+
source: command.source,
|
|
193
|
+
subject: command.id,
|
|
194
|
+
data: { command },
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function deliverTask(input: DeliverTaskInput, ctx: AuthContext): Promise<Record<string, unknown>> {
|
|
199
|
+
const actor = callerLabel(ctx);
|
|
200
|
+
const taskId = resolveTaskId(ctx, input.taskId);
|
|
201
|
+
const before = getTaskDetail(taskId);
|
|
202
|
+
if (!before) throw new ValidationError("task not found");
|
|
203
|
+
if (before.kind !== "deliverable") throw new ValidationError("task not found");
|
|
204
|
+
const wantsLand = input.disposition === "complete" && completeDeliveryLands(before);
|
|
205
|
+
const isVerdictOnlyComplete = input.disposition === "complete" && !wantsLand;
|
|
206
|
+
const gateProblem = input.disposition === "complete" ? completeGateProblem(input.gates) : undefined;
|
|
207
|
+
if (gateProblem) throw new ValidationError(`complete delivery cannot land without passing gate block (${gateProblem})`);
|
|
208
|
+
const reason = input.disposition === "blocked" || input.disposition === "failed" ? cleanReason(input.reason, input.disposition) : undefined;
|
|
209
|
+
const blockedBy = input.disposition === "blocked" ? cleanBlockedBy(input.blockedBy) : input.blockedBy;
|
|
210
|
+
const workspace = wantsLand || input.workspaceId ? resolveWorkspace(ctx, input, taskId) : undefined;
|
|
211
|
+
if (wantsLand && (!workspace || workspace.mode !== "isolated" || !workspace.worktreePath)) {
|
|
212
|
+
throw new ValidationError("complete delivery requires a live isolated workspace");
|
|
213
|
+
}
|
|
214
|
+
const landMeta: TaskDeliverableEventMetadata["land"] | undefined = wantsLand && workspace
|
|
215
|
+
? { requested: true, triggered: false, reason: "pending", workspaceId: workspace.id, workspaceStatus: workspace.status }
|
|
216
|
+
: undefined;
|
|
217
|
+
const delivery = recordTaskDeliverable(taskId, {
|
|
218
|
+
...input,
|
|
219
|
+
actor,
|
|
220
|
+
...(reason ? { reason } : {}),
|
|
221
|
+
...(blockedBy ? { blockedBy } : {}),
|
|
222
|
+
...(workspace ? { workspaceId: workspace.id } : {}),
|
|
223
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
224
|
+
...(landMeta ? { land: landMeta } : {}),
|
|
225
|
+
});
|
|
226
|
+
const eventMeta = deliveryEvent(taskId, input.idempotencyKey)?.metadata;
|
|
227
|
+
if (delivery.created) {
|
|
228
|
+
const deliveryKey = input.idempotencyKey ?? `event:${delivery.event.id}`;
|
|
229
|
+
for (const gate of gateWrites(input.gates, deliveryKey)) {
|
|
230
|
+
recordGate(taskId, { ...gate, actor });
|
|
231
|
+
}
|
|
232
|
+
if (input.disposition === "blocked") {
|
|
233
|
+
setTaskStatus(taskId, { status: "blocked", blockedBy: blockedBy as TaskBlocker[], note: reason, actor });
|
|
234
|
+
} else if (input.disposition === "failed") {
|
|
235
|
+
setTaskStatus(taskId, { status: "failed", note: reason, actor });
|
|
236
|
+
} else if (isVerdictOnlyComplete) {
|
|
237
|
+
setTaskStatus(taskId, { status: "done", note: "complete delivery recorded without land", actor });
|
|
238
|
+
}
|
|
239
|
+
const detail = getTaskDetail(taskId);
|
|
240
|
+
if (detail) {
|
|
241
|
+
emitTaskChanged(detail, "task.deliverable");
|
|
242
|
+
notifyDelivery(taskId, delivery.event.id, eventMeta ?? (delivery.event.metadata as unknown as TaskDeliverableEventMetadata));
|
|
243
|
+
if (input.disposition === "failed") {
|
|
244
|
+
emitTaskSemanticNotification({ task: detail, transition: "gate-failed", actor, occurrenceKey: `event:${delivery.event.id}`, outcome: { disposition: "failed" } });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let land: Record<string, unknown> | undefined;
|
|
250
|
+
if (wantsLand && delivery.created && workspace) {
|
|
251
|
+
markAgentWorkFinal(actor, "task-deliver");
|
|
252
|
+
const result = await applyWorkspaceAction(workspace, {
|
|
253
|
+
action: "ready",
|
|
254
|
+
agentId: actor,
|
|
255
|
+
detail: `Task T${taskId} delivered complete`,
|
|
256
|
+
auditMetadata: { via: "task_deliver", actor, taskId, deliveryEventId: delivery.event.id },
|
|
257
|
+
});
|
|
258
|
+
if (!result.ok) {
|
|
259
|
+
land = { requested: true, triggered: false, error: result.error, workspaceId: workspace.id };
|
|
260
|
+
} else {
|
|
261
|
+
if (result.command) emitCommand(result.command);
|
|
262
|
+
land = { requested: true, triggered: true, workspace: result.workspace, command: result.command };
|
|
263
|
+
}
|
|
264
|
+
} else if (wantsLand && !delivery.created) {
|
|
265
|
+
land = { requested: true, triggered: false, reason: "duplicate-delivery" };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
taskId,
|
|
270
|
+
event: delivery.event,
|
|
271
|
+
created: delivery.created,
|
|
272
|
+
delivery: eventMeta ?? delivery.event.metadata,
|
|
273
|
+
...(land ? { land } : {}),
|
|
274
|
+
};
|
|
275
|
+
}
|
package/src/settings/registry.ts
CHANGED
|
@@ -99,6 +99,7 @@ export function registerSetting<T>(reg: SettingRegistration<T>): SettingDescript
|
|
|
99
99
|
scope: resolveScope(reg.scope),
|
|
100
100
|
tier: reg.tier,
|
|
101
101
|
display: reg.display,
|
|
102
|
+
mergeMapKeys: reg.mergeMapKeys,
|
|
102
103
|
};
|
|
103
104
|
registry.set(id, { descriptor: descriptor as SettingDescriptor, validate });
|
|
104
105
|
return descriptor;
|
|
@@ -235,14 +236,19 @@ const DISPLAY_PREFS_SCHEMA: JSONSchema = {
|
|
|
235
236
|
},
|
|
236
237
|
};
|
|
237
238
|
|
|
238
|
-
// Per-conversation chat scroll memory (#759), tier:"user" so a human's reading position +
|
|
239
|
+
// Per-conversation chat scroll memory (#759/#1124), tier:"user" so a human's reading position +
|
|
239
240
|
// sticky-bottom intent follow them across devices instead of dying in per-browser module state.
|
|
240
241
|
// The value is a MAP keyed by conversation/agent id — one user-settings row holds every
|
|
241
242
|
// conversation's anchor, so this is ONE descriptor (not a per-agent descriptor explosion).
|
|
242
243
|
// Each entry is a LOGICAL anchor, never a pixel scrollTop: `anchorMessageId` is the durable
|
|
243
244
|
// message-id anchor, `topKey` is the compatibility timeline entry key, `offset` the within-row
|
|
244
|
-
// pixel offset, `stick` the follow-the-bottom intent. `
|
|
245
|
-
//
|
|
245
|
+
// pixel offset, `stick` the follow-the-bottom intent. `updatedAt` (epoch ms, #1124) is the
|
|
246
|
+
// per-entry write timestamp the client stamps on every write; it drives both the server's
|
|
247
|
+
// over-cap eviction (oldest-`updatedAt`-first, not oldest-key-first) and the client's
|
|
248
|
+
// newer-wins re-hydration merge, so it is intentionally NOT `required` — older rows written
|
|
249
|
+
// before #1124 lack it and must still validate (they just sort as oldest). `maxProperties`
|
|
250
|
+
// bounds unbounded conversation growth across every device that merges into this row (#1124
|
|
251
|
+
// `mergeMapKeys`); the dashboard store also evicts oldest-touched locally well under it.
|
|
246
252
|
const CHAT_SCROLL_SCHEMA: JSONSchema = {
|
|
247
253
|
type: "object",
|
|
248
254
|
maxProperties: 200,
|
|
@@ -256,6 +262,7 @@ const CHAT_SCROLL_SCHEMA: JSONSchema = {
|
|
|
256
262
|
anchorMessageId: { type: "number", minimum: 0, maximum: 10_000_000_000 },
|
|
257
263
|
anchorEndMessageId: { type: "number", minimum: 0, maximum: 10_000_000_000 },
|
|
258
264
|
offset: { type: "number", minimum: 0, maximum: 10_000_000 },
|
|
265
|
+
updatedAt: { type: "number", minimum: 0 },
|
|
259
266
|
},
|
|
260
267
|
},
|
|
261
268
|
};
|
|
@@ -443,6 +450,9 @@ function seedBuiltInSettings(): void {
|
|
|
443
450
|
});
|
|
444
451
|
|
|
445
452
|
// Per-conversation chat scroll memory (#759) — one row per user, keyed internally by agent id.
|
|
453
|
+
// `mergeMapKeys` (#1124): a PUT merges only the conversation keys it carries into the stored
|
|
454
|
+
// map instead of replacing it, so one device's write can't clobber another device's write to a
|
|
455
|
+
// different conversation (the "desktop tab resurrects every conversation" bug in #1130 chain A2).
|
|
446
456
|
registerSetting({
|
|
447
457
|
namespace: "chat",
|
|
448
458
|
key: "scroll",
|
|
@@ -450,6 +460,7 @@ function seedBuiltInSettings(): void {
|
|
|
450
460
|
defaults: {},
|
|
451
461
|
scope: { read: "settings:read:chat", write: "settings:write:chat" },
|
|
452
462
|
tier: "user",
|
|
463
|
+
mergeMapKeys: true,
|
|
453
464
|
display: { group: "Preferences", label: "Chat scroll", description: "Per-conversation scroll position and sticky-bottom flag (follows you across devices).", order: 72 },
|
|
454
465
|
});
|
|
455
466
|
|
package/src/workspace-phase.ts
CHANGED
|
@@ -84,7 +84,7 @@ export const LAND_PENDING_STALL_MS = 15 * 60 * 1000;
|
|
|
84
84
|
export const DIRTY_WORKTREE_LAND_SKIP_REASON = "worktree has uncommitted changes";
|
|
85
85
|
|
|
86
86
|
type WorkspacePhase =
|
|
87
|
-
| "working" // active — your turn: commit, then
|
|
87
|
+
| "working" // active — your turn: commit, then deliver
|
|
88
88
|
| "land-pending" // ready | review_requested — handed off; auto-merge will land it
|
|
89
89
|
| "landing" // merge_planned — merge dispatched, in progress
|
|
90
90
|
| "steward" // conflict — auto-merge couldn't; a steward is reconciling (not your job)
|
|
@@ -122,13 +122,13 @@ const WAIT_ACTION: WorkspaceNextAction = {
|
|
|
122
122
|
const CHECK_STATUS_ACTION: WorkspaceNextAction = {
|
|
123
123
|
tool: "relay_workspace_status",
|
|
124
124
|
cli: "agent-relay workspace status",
|
|
125
|
-
when: "only if you are explicitly checking state later; do not poll or wait after
|
|
125
|
+
when: "only if you are explicitly checking state later; do not poll or wait after delivering",
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
const READY_ACTION: WorkspaceNextAction = {
|
|
129
|
-
tool: "
|
|
130
|
-
cli: "
|
|
131
|
-
when: "after you commit your work
|
|
129
|
+
tool: "relay_task_deliver",
|
|
130
|
+
cli: "MCP relay_task_deliver",
|
|
131
|
+
when: "after you commit your work and have passing delivery gates",
|
|
132
132
|
};
|
|
133
133
|
|
|
134
134
|
// Map every WorkspaceStatus to the branch agent's mental model. Statuses that
|