@opengeni/api-router 2.5.0 → 2.6.4-canary.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/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-QESX7HDK.js → chunk-UOI7KAP3.js} +3240 -791
- package/dist/chunk-UOI7KAP3.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +117 -5
- package/dist/index.js.map +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +19 -19
- package/src/app.ts +206 -29
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/index.ts +13 -1
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +328 -27
- package/src/mcp/session-wait.ts +56 -8
- package/src/model-catalog.ts +45 -337
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/automations.ts +186 -19
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +0 -8
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/pr-review.ts +80 -4
- package/src/routes/scheduled-tasks.ts +47 -11
- package/src/routes/sessions.ts +75 -53
- package/src/routes/skills.ts +3 -3
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +665 -56
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/workspace-delete-observability.ts +75 -0
- package/dist/chunk-QESX7HDK.js.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
stableJson,
|
|
15
15
|
compactSessionEventResult,
|
|
16
16
|
sessionEventLatestClassToSemanticClass,
|
|
17
|
+
MemorySlackPublicationDistribution,
|
|
17
18
|
SessionMcpCredentialUpdateInput,
|
|
18
19
|
ToolAuthNeededPayload,
|
|
19
20
|
VariableSetVariableName,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
|
|
39
40
|
SESSION_GOAL_TEXT_MAX_BYTES,
|
|
40
41
|
SESSION_INSTRUCTIONS_MAX_CHARACTERS,
|
|
42
|
+
SESSION_TITLE_MAX_CHARACTERS,
|
|
41
43
|
MAX_SELECTED_VARIABLE_SETS,
|
|
42
44
|
sessionGoalUtf8Bytes,
|
|
43
45
|
TASK_NOTE_LIST_DEFAULT_LIMIT,
|
|
@@ -90,6 +92,8 @@ import {
|
|
|
90
92
|
readVariableSetSecretAtomically,
|
|
91
93
|
recordSyncedSocialPosts,
|
|
92
94
|
listVariableSets,
|
|
95
|
+
MEMORY_CORRECT_TOOL_DESCRIPTION,
|
|
96
|
+
MEMORY_SAVE_TOOL_DESCRIPTION,
|
|
93
97
|
MEMORY_SEARCH_TOOL_DESCRIPTION,
|
|
94
98
|
requireScheduledTask,
|
|
95
99
|
requireSession,
|
|
@@ -119,7 +123,11 @@ import {
|
|
|
119
123
|
acceptSessionHumanInputResponse,
|
|
120
124
|
HumanInputResponseValidationError,
|
|
121
125
|
} from "@opengeni/db";
|
|
122
|
-
import {
|
|
126
|
+
import {
|
|
127
|
+
appendAndPublishEvents,
|
|
128
|
+
appendAndPublishTurnEventsFenced,
|
|
129
|
+
publishDurableSessionEvents,
|
|
130
|
+
} from "@opengeni/events";
|
|
123
131
|
import { allowedFirstPartyMcpToolsForSession, codemodeWorkspaceUrl } from "@opengeni/config";
|
|
124
132
|
import {
|
|
125
133
|
createSignedState,
|
|
@@ -141,12 +149,15 @@ import {
|
|
|
141
149
|
authorizedSocialConnectionsForGrant,
|
|
142
150
|
authorizedAtlassianConnectionsForGrant,
|
|
143
151
|
buildCapabilityCatalog,
|
|
152
|
+
correctWorkspaceMemoryWithSlackPublication,
|
|
144
153
|
nativeConnectionCapabilityRecommendations,
|
|
145
154
|
requireLiveAgentAttemptAuthorization,
|
|
146
155
|
requireSessionAuthorization,
|
|
147
156
|
requireSessionAuthorizationListScope,
|
|
157
|
+
resolveWorkspaceCatalogSettings,
|
|
148
158
|
SessionAuthorizationDeniedError,
|
|
149
159
|
SessionAuthorizationUnavailableError,
|
|
160
|
+
saveWorkspaceMemoryWithSlackPublication,
|
|
150
161
|
searchCapabilityCatalogItems,
|
|
151
162
|
type ResolvedSessionAuthorization,
|
|
152
163
|
} from "@opengeni/core";
|
|
@@ -191,6 +202,7 @@ import {
|
|
|
191
202
|
ScheduledTaskSyncError,
|
|
192
203
|
syncCreatedScheduledTask,
|
|
193
204
|
syncUpdatedScheduledTask,
|
|
205
|
+
validateScheduledTaskMachineTarget,
|
|
194
206
|
validateScheduledTaskTarget,
|
|
195
207
|
updateScheduledTaskForApi,
|
|
196
208
|
validatedScheduledTaskUpdate,
|
|
@@ -228,11 +240,13 @@ import {
|
|
|
228
240
|
SESSION_EVENT_MCP_MAX_BYTES,
|
|
229
241
|
} from "./session-view";
|
|
230
242
|
import {
|
|
243
|
+
SESSION_WAIT_COMPLETION_EVENT_TYPES,
|
|
231
244
|
SESSION_WAIT_DEFAULT_SECONDS,
|
|
232
245
|
SESSION_WAIT_EVENT_TYPES,
|
|
233
246
|
SESSION_WAIT_EVENTS_PER_TARGET,
|
|
234
247
|
SESSION_WAIT_MAX_SECONDS,
|
|
235
248
|
SESSION_WAIT_MAX_TARGETS,
|
|
249
|
+
sessionWaitCompletionEventMatches,
|
|
236
250
|
waitForSessionChanges,
|
|
237
251
|
} from "./session-wait";
|
|
238
252
|
import {
|
|
@@ -275,6 +289,12 @@ export type McpServerOptions = {
|
|
|
275
289
|
|
|
276
290
|
const ORCHESTRATION_FAILURE_CODE_MAX_LENGTH = 128;
|
|
277
291
|
const ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1_024;
|
|
292
|
+
// Keep pathological raw MCP payloads away from Unicode normalization while
|
|
293
|
+
// leaving the shared DB normalizer authoritative for the exact post-NFKC
|
|
294
|
+
// code-point limit. The multiplier admits supplementary-plane characters,
|
|
295
|
+
// decomposed forms, and ordinary whitespace folding without reopening the
|
|
296
|
+
// API-wide request-body ceiling for this 200-code-point field.
|
|
297
|
+
const MCP_DISCOVERY_QUERY_MAX_UTF16_CODE_UNITS = WORK_DISCOVERY_QUERY_MAX_CHARS * 8;
|
|
278
298
|
|
|
279
299
|
type OrchestrationToolName = "session_create" | "session_send_message";
|
|
280
300
|
|
|
@@ -409,9 +429,6 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
409
429
|
goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
410
430
|
goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
411
431
|
memory_search: { sessionRequired: true, allOf: ["documents:search"] },
|
|
412
|
-
// Retired: never registered, so these are never consulted. The map must stay
|
|
413
|
-
// total over the tool-name union, which still carries both names so that
|
|
414
|
-
// previously written scheduled-task snapshots keep parsing.
|
|
415
432
|
memory_save: { sessionRequired: true, allOf: ["documents:search"] },
|
|
416
433
|
memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
|
|
417
434
|
preference_registry_summary: {
|
|
@@ -1702,7 +1719,7 @@ export function buildOpenGeniMcpServer(
|
|
|
1702
1719
|
async ({ id, triggerId }) => {
|
|
1703
1720
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1704
1721
|
if (task.action.kind === "agent_turn") {
|
|
1705
|
-
await validateScheduledTaskTarget({
|
|
1722
|
+
const targetSession = await validateScheduledTaskTarget({
|
|
1706
1723
|
db: deps.db,
|
|
1707
1724
|
sessionAuthorization: deps.sessionAuthorization,
|
|
1708
1725
|
authorizationSurface: "first_party_mcp",
|
|
@@ -1714,6 +1731,22 @@ export function buildOpenGeniMcpServer(
|
|
|
1714
1731
|
agentConfig: task.agentConfig,
|
|
1715
1732
|
missingTargetStatus: 404,
|
|
1716
1733
|
});
|
|
1734
|
+
const catalogSourceSettings = deps.catalogSourceSettings ?? deps.settings;
|
|
1735
|
+
const catalogSettings = (
|
|
1736
|
+
await resolveWorkspaceCatalogSettings(deps.db, catalogSourceSettings, {
|
|
1737
|
+
accountId: grant.accountId,
|
|
1738
|
+
workspaceId: grant.workspaceId,
|
|
1739
|
+
...(targetSession ? { retainedProductModelId: targetSession.model } : {}),
|
|
1740
|
+
})
|
|
1741
|
+
).settings;
|
|
1742
|
+
await validateScheduledTaskMachineTarget({
|
|
1743
|
+
settings: catalogSettings,
|
|
1744
|
+
db: deps.db,
|
|
1745
|
+
grant,
|
|
1746
|
+
runMode: task.runMode,
|
|
1747
|
+
agentConfig: task.agentConfig,
|
|
1748
|
+
requireOnline: true,
|
|
1749
|
+
});
|
|
1717
1750
|
await requireLimit(deps, {
|
|
1718
1751
|
accountId: grant.accountId,
|
|
1719
1752
|
workspaceId: grant.workspaceId,
|
|
@@ -3425,6 +3458,7 @@ function registerPreferenceRegistryTools(
|
|
|
3425
3458
|
}
|
|
3426
3459
|
|
|
3427
3460
|
const MemoryKindSchema = z4.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
|
|
3461
|
+
const MemoryWriteKindSchema = z4.enum(["semantic", "decision", "episodic"]);
|
|
3428
3462
|
|
|
3429
3463
|
function scheduledTaskReceipt(
|
|
3430
3464
|
operation: string,
|
|
@@ -3516,6 +3550,11 @@ export function memorySlackPublicationActor(
|
|
|
3516
3550
|
};
|
|
3517
3551
|
}
|
|
3518
3552
|
|
|
3553
|
+
function memoryPreview(text: string): string {
|
|
3554
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
3555
|
+
return normalized.length <= 120 ? normalized : `${normalized.slice(0, 119)}…`;
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3519
3558
|
function registerMemoryTools(
|
|
3520
3559
|
server: McpServer,
|
|
3521
3560
|
deps: ApiRouteDeps,
|
|
@@ -3524,10 +3563,17 @@ function registerMemoryTools(
|
|
|
3524
3563
|
json: JsonResult,
|
|
3525
3564
|
promptMode: WorkspaceMemoryPromptMode,
|
|
3526
3565
|
): void {
|
|
3566
|
+
const publicationInputSchema = z4.object({
|
|
3567
|
+
importance: z4.enum(["major", "normal", "minor"]),
|
|
3568
|
+
audience: z4.literal("workspace"),
|
|
3569
|
+
slackMode: z4.enum(["auto", "review", "never"]),
|
|
3570
|
+
shareSummary: z4.string().trim().min(1).max(4_096),
|
|
3571
|
+
});
|
|
3572
|
+
|
|
3527
3573
|
server.registerTool(
|
|
3528
3574
|
"memory_search",
|
|
3529
3575
|
{
|
|
3530
|
-
description: `${MEMORY_SEARCH_TOOL_DESCRIPTION} Legacy preference
|
|
3576
|
+
description: `${MEMORY_SEARCH_TOOL_DESCRIPTION} All existing Memory kinds are searchable. Legacy preference and procedure records are historical context, not active instructions; Skills and workspace instructions remain the behavioral authorities. When workspace Memory is enabled, use memory_save autonomously for durable facts, decisions, incidents, fixes, and outcomes, and memory_correct when an existing record is wrong or outdated.`,
|
|
3531
3577
|
inputSchema: {
|
|
3532
3578
|
query: z4.string().min(1),
|
|
3533
3579
|
kind: MemoryKindSchema.optional(),
|
|
@@ -3550,10 +3596,239 @@ function registerMemoryTools(
|
|
|
3550
3596
|
}),
|
|
3551
3597
|
);
|
|
3552
3598
|
|
|
3553
|
-
// Memory
|
|
3554
|
-
//
|
|
3555
|
-
|
|
3556
|
-
|
|
3599
|
+
// Memory writes are agent-only. Human creation and curation use the REST/UI
|
|
3600
|
+
// surface; a non-attempt MCP principal may search but cannot mutate Memory.
|
|
3601
|
+
if (exactAgentAttemptClaims(grant) === null) return;
|
|
3602
|
+
|
|
3603
|
+
server.registerTool(
|
|
3604
|
+
"memory_save",
|
|
3605
|
+
{
|
|
3606
|
+
description: MEMORY_SAVE_TOOL_DESCRIPTION,
|
|
3607
|
+
inputSchema: {
|
|
3608
|
+
text: z4.string().min(1),
|
|
3609
|
+
kind: MemoryWriteKindSchema,
|
|
3610
|
+
confidence: z4.number().min(0).max(1).optional(),
|
|
3611
|
+
replaces_id: z4.string().min(1).optional(),
|
|
3612
|
+
slack_publication: publicationInputSchema.optional(),
|
|
3613
|
+
},
|
|
3614
|
+
},
|
|
3615
|
+
async ({ text, kind, confidence, replaces_id, slack_publication }) => {
|
|
3616
|
+
const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
3617
|
+
const result = await saveWorkspaceMemoryWithSlackPublication(
|
|
3618
|
+
deps.db,
|
|
3619
|
+
{
|
|
3620
|
+
accountId: grant.accountId,
|
|
3621
|
+
workspaceId: grant.workspaceId,
|
|
3622
|
+
sessionId,
|
|
3623
|
+
text,
|
|
3624
|
+
kind,
|
|
3625
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
3626
|
+
...(replaces_id ? { replacesId: replaces_id } : {}),
|
|
3627
|
+
origin: "agent",
|
|
3628
|
+
},
|
|
3629
|
+
slack_publication
|
|
3630
|
+
? {
|
|
3631
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
3632
|
+
actor: memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null)
|
|
3633
|
+
.actor,
|
|
3634
|
+
ownerLabel: actor.initiator.label ?? grant.subjectLabel ?? null,
|
|
3635
|
+
}
|
|
3636
|
+
: null,
|
|
3637
|
+
deps.getDocumentServices().embedder,
|
|
3638
|
+
);
|
|
3639
|
+
let timelineWarning: string | null = null;
|
|
3640
|
+
try {
|
|
3641
|
+
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
3642
|
+
{
|
|
3643
|
+
type: "memory.saved",
|
|
3644
|
+
payload: {
|
|
3645
|
+
memoryId: result.memory.id,
|
|
3646
|
+
kind: result.memory.kind,
|
|
3647
|
+
preview: memoryPreview(result.memory.text),
|
|
3648
|
+
deduped: result.deduped,
|
|
3649
|
+
...(result.superseded ? { supersededMemoryId: result.superseded.id } : {}),
|
|
3650
|
+
},
|
|
3651
|
+
},
|
|
3652
|
+
]);
|
|
3653
|
+
} catch {
|
|
3654
|
+
timelineWarning = "Memory committed, but its session timeline event could not be recorded.";
|
|
3655
|
+
console.warn("workspace memory save: committed without session timeline event", {
|
|
3656
|
+
errorClass: "MemoryTimelineOperationError",
|
|
3657
|
+
errorCode: "memory_save_timeline_append_failed",
|
|
3658
|
+
origin: "api",
|
|
3659
|
+
workspaceId: grant.workspaceId,
|
|
3660
|
+
sessionId,
|
|
3661
|
+
memoryId: result.memory.id,
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
const changed = !result.deduped || result.updated || result.superseded !== null;
|
|
3665
|
+
const outcome =
|
|
3666
|
+
result.updated || result.superseded !== null
|
|
3667
|
+
? "updated"
|
|
3668
|
+
: result.deduped
|
|
3669
|
+
? "unchanged"
|
|
3670
|
+
: "created";
|
|
3671
|
+
return json(
|
|
3672
|
+
mcpMutationReceipt({
|
|
3673
|
+
operation: "memory_save",
|
|
3674
|
+
committed: true,
|
|
3675
|
+
outcome,
|
|
3676
|
+
changed,
|
|
3677
|
+
resource: {
|
|
3678
|
+
type: "knowledge_memory",
|
|
3679
|
+
id: result.memory.id,
|
|
3680
|
+
state: result.memory.status,
|
|
3681
|
+
},
|
|
3682
|
+
relatedResources: result.superseded
|
|
3683
|
+
? [
|
|
3684
|
+
{
|
|
3685
|
+
type: "knowledge_memory",
|
|
3686
|
+
id: result.superseded.id,
|
|
3687
|
+
state: result.superseded.status,
|
|
3688
|
+
},
|
|
3689
|
+
]
|
|
3690
|
+
: undefined,
|
|
3691
|
+
timestamp: result.memory.updatedAt,
|
|
3692
|
+
idempotency: { status: "not_supported" },
|
|
3693
|
+
warnings: [
|
|
3694
|
+
...(!result.embedded
|
|
3695
|
+
? ["Memory committed without a vector embedding; keyword search remains available."]
|
|
3696
|
+
: []),
|
|
3697
|
+
...(timelineWarning ? [timelineWarning] : []),
|
|
3698
|
+
],
|
|
3699
|
+
facts: {
|
|
3700
|
+
deduped: result.deduped,
|
|
3701
|
+
dedupeReason: result.dedupeReason,
|
|
3702
|
+
updatedInPlace: result.updated,
|
|
3703
|
+
embedded: result.embedded,
|
|
3704
|
+
slackPublicationDecision: result.slackPublication.decision?.eligible
|
|
3705
|
+
? "eligible"
|
|
3706
|
+
: (result.slackPublication.decision?.reason ?? "not_requested"),
|
|
3707
|
+
slackPublicationId:
|
|
3708
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3709
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3710
|
+
? result.slackPublication.enqueue.publication.id
|
|
3711
|
+
: null,
|
|
3712
|
+
slackPublicationState:
|
|
3713
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3714
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3715
|
+
? result.slackPublication.enqueue.publication.state
|
|
3716
|
+
: null,
|
|
3717
|
+
},
|
|
3718
|
+
}),
|
|
3719
|
+
);
|
|
3720
|
+
},
|
|
3721
|
+
);
|
|
3722
|
+
|
|
3723
|
+
server.registerTool(
|
|
3724
|
+
"memory_correct",
|
|
3725
|
+
{
|
|
3726
|
+
description: MEMORY_CORRECT_TOOL_DESCRIPTION,
|
|
3727
|
+
inputSchema: {
|
|
3728
|
+
id: z4.string().min(1),
|
|
3729
|
+
reason: z4.string().min(1).optional(),
|
|
3730
|
+
replacement_text: z4.string().min(1).optional(),
|
|
3731
|
+
slack_publication: publicationInputSchema.optional(),
|
|
3732
|
+
},
|
|
3733
|
+
},
|
|
3734
|
+
async ({ id, reason, replacement_text, slack_publication }) => {
|
|
3735
|
+
const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
3736
|
+
const result = await correctWorkspaceMemoryWithSlackPublication(
|
|
3737
|
+
deps.db,
|
|
3738
|
+
{
|
|
3739
|
+
accountId: grant.accountId,
|
|
3740
|
+
workspaceId: grant.workspaceId,
|
|
3741
|
+
sessionId,
|
|
3742
|
+
id,
|
|
3743
|
+
...(reason ? { reason } : {}),
|
|
3744
|
+
...(replacement_text ? { replacementText: replacement_text } : {}),
|
|
3745
|
+
origin: "agent",
|
|
3746
|
+
},
|
|
3747
|
+
slack_publication
|
|
3748
|
+
? {
|
|
3749
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
3750
|
+
actor: memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null)
|
|
3751
|
+
.actor,
|
|
3752
|
+
ownerLabel: actor.initiator.label ?? grant.subjectLabel ?? null,
|
|
3753
|
+
}
|
|
3754
|
+
: null,
|
|
3755
|
+
deps.getDocumentServices().embedder,
|
|
3756
|
+
);
|
|
3757
|
+
let timelineWarning: string | null = null;
|
|
3758
|
+
try {
|
|
3759
|
+
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
3760
|
+
{
|
|
3761
|
+
type: "memory.corrected",
|
|
3762
|
+
payload: {
|
|
3763
|
+
memoryId: result.memory.id,
|
|
3764
|
+
kind: result.memory.kind,
|
|
3765
|
+
preview: memoryPreview(result.memory.text),
|
|
3766
|
+
action: result.action,
|
|
3767
|
+
...(reason ? { reason: memoryPreview(reason) } : {}),
|
|
3768
|
+
...(result.replacement
|
|
3769
|
+
? {
|
|
3770
|
+
replacementMemoryId: result.replacement.id,
|
|
3771
|
+
replacementPreview: memoryPreview(result.replacement.text),
|
|
3772
|
+
}
|
|
3773
|
+
: {}),
|
|
3774
|
+
},
|
|
3775
|
+
},
|
|
3776
|
+
]);
|
|
3777
|
+
} catch {
|
|
3778
|
+
timelineWarning =
|
|
3779
|
+
"Memory correction committed, but its session timeline event could not be recorded.";
|
|
3780
|
+
console.warn("workspace memory correction: committed without session timeline event", {
|
|
3781
|
+
errorClass: "MemoryTimelineOperationError",
|
|
3782
|
+
errorCode: "memory_correct_timeline_append_failed",
|
|
3783
|
+
origin: "api",
|
|
3784
|
+
workspaceId: grant.workspaceId,
|
|
3785
|
+
sessionId,
|
|
3786
|
+
memoryId: result.memory.id,
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
return json(
|
|
3790
|
+
mcpMutationReceipt({
|
|
3791
|
+
operation: "memory_correct",
|
|
3792
|
+
committed: true,
|
|
3793
|
+
outcome: "updated",
|
|
3794
|
+
changed: true,
|
|
3795
|
+
resource: {
|
|
3796
|
+
type: "knowledge_memory",
|
|
3797
|
+
id: result.memory.id,
|
|
3798
|
+
state: result.memory.status,
|
|
3799
|
+
},
|
|
3800
|
+
relatedResources: result.replacement
|
|
3801
|
+
? [
|
|
3802
|
+
{
|
|
3803
|
+
type: "knowledge_memory",
|
|
3804
|
+
id: result.replacement.id,
|
|
3805
|
+
state: result.replacement.status,
|
|
3806
|
+
},
|
|
3807
|
+
]
|
|
3808
|
+
: undefined,
|
|
3809
|
+
timestamp: (result.replacement ?? result.memory).updatedAt,
|
|
3810
|
+
idempotency: { status: "not_supported" },
|
|
3811
|
+
warnings: timelineWarning ? [timelineWarning] : [],
|
|
3812
|
+
facts: {
|
|
3813
|
+
correctionAction: result.action,
|
|
3814
|
+
slackPublicationDecision: result.slackPublication.decision?.eligible
|
|
3815
|
+
? "eligible"
|
|
3816
|
+
: (result.slackPublication.decision?.reason ?? "not_requested"),
|
|
3817
|
+
slackPublicationId:
|
|
3818
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3819
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3820
|
+
? result.slackPublication.enqueue.publication.id
|
|
3821
|
+
: null,
|
|
3822
|
+
slackPublicationState:
|
|
3823
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3824
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3825
|
+
? result.slackPublication.enqueue.publication.state
|
|
3826
|
+
: null,
|
|
3827
|
+
},
|
|
3828
|
+
}),
|
|
3829
|
+
);
|
|
3830
|
+
},
|
|
3831
|
+
);
|
|
3557
3832
|
}
|
|
3558
3833
|
|
|
3559
3834
|
// Fleet tools (M7 bring-your-own-compute). Session-scoped (they steer THIS
|
|
@@ -4155,7 +4430,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4155
4430
|
includeLastMessage: z4.boolean().optional(),
|
|
4156
4431
|
orderBy: z4.enum(["createdAt", "updatedAt", "relevance"]).optional(),
|
|
4157
4432
|
updatedAfter: z4.string().max(64).optional(),
|
|
4158
|
-
query: z4.string().max(
|
|
4433
|
+
query: z4.string().max(MCP_DISCOVERY_QUERY_MAX_UTF16_CODE_UNITS).optional(),
|
|
4159
4434
|
statuses: z4
|
|
4160
4435
|
.array(
|
|
4161
4436
|
z4.enum([
|
|
@@ -4443,7 +4718,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4443
4718
|
server.registerTool(
|
|
4444
4719
|
"session_wait",
|
|
4445
4720
|
{
|
|
4446
|
-
description: `Block until a watched session has new durable events after your cursor, until your own session has pending machine input (a child result, an agent message, a steer), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for short waits inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works; for long waits end this turn with goal_wait rather than looping session_wait for hours while holding the turn and sandbox. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session)
|
|
4721
|
+
description: `Block until a watched session has new durable events after your cursor, until your own session has pending machine input (a child result, an agent message, a steer), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for short waits inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works; for long waits end this turn with goal_wait rather than looping session_wait for hours while holding the turn and sandbox. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session). waitFor=change is the backward-compatible default and returns on turn lifecycle, agent.message.completed, blocking failures, goal facts, or session status/control changes. waitFor=completion is the child-result join: it ignores progress, completed commentary messages, goal facts, maintenance turns, and continuation segment settlements and returns only for a result-bearing final turn or a blocking state. A goal.completed event records goal state but is not a terminal child result. Raw deltas, tool receipts, sandbox diagnostics, and unrelated progress never wake either mode. Each changed target returns a bounded compact summary of up to ${SESSION_WAIT_EVENTS_PER_TARGET} exact durable events plus latestSequence (pass it back as the next afterSequence) and hasMore (drill down with session_events after=latestSequence). ownPendingUpdates > 0 means your own session has machine input that is delivered only when your next turn is claimed: finish this turn to receive it, or pass includeOwnPendingUpdates=false to keep waiting on the targets. timedOut=true means nothing changed; liveFanout=false means the live bus was unavailable and the wait relied on the deadline re-check. The whole result is byte-bounded: summaries are shortened first, then newest rows dropped, so a changed target may come back with events=[] and hasMore=true; read those rows with session_events after=latestSequence. The wait cannot exceed ${SESSION_WAIT_MAX_SECONDS} seconds because the MCP client request timeout is 60 seconds.`,
|
|
4447
4722
|
inputSchema: {
|
|
4448
4723
|
targets: z4
|
|
4449
4724
|
.array(
|
|
@@ -4460,10 +4735,16 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4460
4735
|
.describe(
|
|
4461
4736
|
"Also return when your own session has pending machine input (default true).",
|
|
4462
4737
|
),
|
|
4738
|
+
waitFor: z4
|
|
4739
|
+
.enum(["change", "completion"])
|
|
4740
|
+
.optional()
|
|
4741
|
+
.describe(
|
|
4742
|
+
"change (default) returns on relevant activity; completion ignores messages, goal/progress, maintenance, and continuation segments until a result-bearing final turn or blocker.",
|
|
4743
|
+
),
|
|
4463
4744
|
maxWaitSeconds: z4.number().int().min(1).max(SESSION_WAIT_MAX_SECONDS).optional(),
|
|
4464
4745
|
},
|
|
4465
4746
|
},
|
|
4466
|
-
async ({ targets, includeOwnPendingUpdates, maxWaitSeconds }, extra) => {
|
|
4747
|
+
async ({ targets, includeOwnPendingUpdates, waitFor, maxWaitSeconds }, extra) => {
|
|
4467
4748
|
const distinct = new Set(targets.map((target) => target.sessionId));
|
|
4468
4749
|
if (distinct.size !== targets.length) {
|
|
4469
4750
|
throw new Error("session_wait targets must name distinct sessions");
|
|
@@ -4475,6 +4756,8 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4475
4756
|
await requireSession(deps.db, grant.workspaceId, target.sessionId);
|
|
4476
4757
|
}
|
|
4477
4758
|
const ownSessionId = includeOwnPendingUpdates === false ? null : callerSessionId;
|
|
4759
|
+
const targetEventTypes =
|
|
4760
|
+
waitFor === "completion" ? SESSION_WAIT_COMPLETION_EVENT_TYPES : SESSION_WAIT_EVENT_TYPES;
|
|
4478
4761
|
// The API serves one transport per POST, so the worker's MCP cancel
|
|
4479
4762
|
// notification never reaches this handler; the route binds the HTTP
|
|
4480
4763
|
// request's abort to transport.close() (mcp/request-abort.ts), which
|
|
@@ -4491,6 +4774,9 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4491
4774
|
targets,
|
|
4492
4775
|
ownSessionId,
|
|
4493
4776
|
maxWaitMs: (maxWaitSeconds ?? SESSION_WAIT_DEFAULT_SECONDS) * 1_000,
|
|
4777
|
+
targetEventTypes,
|
|
4778
|
+
targetEventMatches:
|
|
4779
|
+
waitFor === "completion" ? sessionWaitCompletionEventMatches : undefined,
|
|
4494
4780
|
signal,
|
|
4495
4781
|
source: {
|
|
4496
4782
|
reauthorizeTargets: async (sessionIds) => {
|
|
@@ -4509,7 +4795,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4509
4795
|
direction: "after",
|
|
4510
4796
|
limit: SESSION_WAIT_EVENTS_PER_TARGET,
|
|
4511
4797
|
payloadMode: "full",
|
|
4512
|
-
includeTypes:
|
|
4798
|
+
includeTypes: targetEventTypes,
|
|
4513
4799
|
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
|
|
4514
4800
|
});
|
|
4515
4801
|
return { events: page.events, hasMore: page.hasMore };
|
|
@@ -4538,6 +4824,14 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4538
4824
|
const sessionCreateInput = z4
|
|
4539
4825
|
.object({
|
|
4540
4826
|
initialMessage: z4.string().min(1),
|
|
4827
|
+
title: z4
|
|
4828
|
+
.string()
|
|
4829
|
+
.min(1)
|
|
4830
|
+
.max(SESSION_TITLE_MAX_CHARACTERS)
|
|
4831
|
+
.optional()
|
|
4832
|
+
.describe(
|
|
4833
|
+
"Concise semantic title for the child session. Omit only when the delegated goal or initial message already provides a suitable title; OpenGeni derives a sensitive-safe bounded fallback from that text.",
|
|
4834
|
+
),
|
|
4541
4835
|
instructions: z4.string().min(1).max(SESSION_INSTRUCTIONS_MAX_CHARACTERS).optional(),
|
|
4542
4836
|
goal: z4.unknown().optional(),
|
|
4543
4837
|
resources: z4.array(z4.unknown()).optional(),
|
|
@@ -4620,7 +4914,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4620
4914
|
"session_create",
|
|
4621
4915
|
{
|
|
4622
4916
|
description:
|
|
4623
|
-
"Spawn a new agent session (a worker). The child inherits this session's visibility; a private session can only create a same-owner private child. Give a goal-bearing child its delegated objective. Its goal.rootConstraints may be an exact applicable subset of this accepted turn's frozen root constraints; omit that field to inherit all of them. Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set, Rig, or machineTarget gets its own box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget; a machineTarget is always an own-box create even when the parent is backend none. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
|
|
4917
|
+
"Spawn a new agent session (a worker) only for a concrete, bounded subtask that can run independently and has a defined integration point in your current work. Do not delegate work you will also perform yourself; track the child and join its actual result before completing dependent work. Give the child a concise semantic title; if omitted, OpenGeni derives one from its delegated goal or initial message. The child inherits this session's visibility; a private session can only create a same-owner private child. Give a goal-bearing child its delegated objective. Its goal.rootConstraints may be an exact applicable subset of this accepted turn's frozen root constraints; omit that field to inherit all of them. Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set, Rig, or machineTarget gets its own box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget; a machineTarget is always an own-box create even when the parent is backend none. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
|
|
4624
4918
|
inputSchema: sessionCreateInput,
|
|
4625
4919
|
},
|
|
4626
4920
|
async (args) => {
|
|
@@ -4633,18 +4927,25 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4633
4927
|
if (callerSessionId !== null) {
|
|
4634
4928
|
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
4635
4929
|
}
|
|
4636
|
-
const { machineTarget, ...request } = args;
|
|
4637
|
-
const result = await createSessionForRequestWithOutcome(
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4930
|
+
const { machineTarget, title, ...request } = args;
|
|
4931
|
+
const result = await createSessionForRequestWithOutcome(
|
|
4932
|
+
deps,
|
|
4933
|
+
grant,
|
|
4934
|
+
grant.workspaceId,
|
|
4935
|
+
{
|
|
4936
|
+
...request,
|
|
4937
|
+
...(machineTarget
|
|
4938
|
+
? {
|
|
4939
|
+
targetSandboxId: machineTarget.targetSandboxId,
|
|
4940
|
+
...(machineTarget.workingDir !== undefined
|
|
4941
|
+
? { workingDir: machineTarget.workingDir }
|
|
4942
|
+
: {}),
|
|
4943
|
+
}
|
|
4944
|
+
: {}),
|
|
4945
|
+
},
|
|
4946
|
+
undefined,
|
|
4947
|
+
title === undefined ? {} : { automaticTitleCandidate: title },
|
|
4948
|
+
);
|
|
4648
4949
|
return json(sessionCreateMutationReceipt(result, Boolean(request.idempotencyKey)));
|
|
4649
4950
|
} catch (error) {
|
|
4650
4951
|
return orchestrationFailureResult("session_create", error);
|
package/src/mcp/session-wait.ts
CHANGED
|
@@ -81,7 +81,51 @@ export const SESSION_WAIT_EVENT_TYPES = [
|
|
|
81
81
|
"goal.continuation",
|
|
82
82
|
] as const satisfies readonly SessionEventType[];
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Settlement and blocking events that make a child result usable.
|
|
86
|
+
* Goal facts are deliberately absent: an agent can complete its durable goal
|
|
87
|
+
* before it emits the final assistant message and settles the turn. Completed
|
|
88
|
+
* agent messages are also absent because commentary messages use the same
|
|
89
|
+
* event type; an ordinary result-bearing `turn.completed` carries the
|
|
90
|
+
* authoritative final output.
|
|
91
|
+
*/
|
|
92
|
+
export const SESSION_WAIT_COMPLETION_EVENT_TYPES = [
|
|
93
|
+
"turn.completed",
|
|
94
|
+
"turn.failed",
|
|
95
|
+
"turn.cancelled",
|
|
96
|
+
"turn.superseded",
|
|
97
|
+
"turn.capacity_waiting",
|
|
98
|
+
"session.requiresAction",
|
|
99
|
+
"session.humanInput.requested",
|
|
100
|
+
"session.control.paused",
|
|
101
|
+
"tool.auth_needed",
|
|
102
|
+
"credential.auth_needed",
|
|
103
|
+
"rig.setup.failed",
|
|
104
|
+
"goal.paused",
|
|
105
|
+
] as const satisfies readonly SessionEventType[];
|
|
106
|
+
|
|
107
|
+
const SESSION_WAIT_COMPLETION_EVENT_TYPE_SET: ReadonlySet<string> = new Set(
|
|
108
|
+
SESSION_WAIT_COMPLETION_EVENT_TYPES,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A completed turn is result-bearing only when it carries the ordinary final
|
|
113
|
+
* output. Segment-limit and maintenance turns settle one execution segment
|
|
114
|
+
* while the session still has work to do, so they must not release a parent.
|
|
115
|
+
*/
|
|
116
|
+
export function sessionWaitCompletionEventMatches(event: SessionEvent): boolean {
|
|
117
|
+
if (!SESSION_WAIT_COMPLETION_EVENT_TYPE_SET.has(event.type)) return false;
|
|
118
|
+
if (event.type !== "turn.completed") return true;
|
|
119
|
+
if (event.payload === null || typeof event.payload !== "object" || Array.isArray(event.payload)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const payload = event.payload as Record<string, unknown>;
|
|
123
|
+
return (
|
|
124
|
+
Object.prototype.hasOwnProperty.call(payload, "output") &&
|
|
125
|
+
!Object.prototype.hasOwnProperty.call(payload, "segmentLimit") &&
|
|
126
|
+
!Object.prototype.hasOwnProperty.call(payload, "maintenance")
|
|
127
|
+
);
|
|
128
|
+
}
|
|
85
129
|
|
|
86
130
|
/** The self-session event that announces a newly pending machine input. */
|
|
87
131
|
export const SESSION_WAIT_OWN_PENDING_EVENT_TYPE =
|
|
@@ -188,6 +232,10 @@ export type SessionWaitInput = {
|
|
|
188
232
|
targets: readonly SessionWaitTarget[];
|
|
189
233
|
ownSessionId: string | null;
|
|
190
234
|
maxWaitMs: number;
|
|
235
|
+
/** Target events that end the wait. Defaults to the ordinary activity set. */
|
|
236
|
+
targetEventTypes?: readonly SessionEventType[] | undefined;
|
|
237
|
+
/** Optional payload-aware refinement applied after the event-type filter. */
|
|
238
|
+
targetEventMatches?: ((event: SessionEvent) => boolean) | undefined;
|
|
191
239
|
source: SessionWaitSource;
|
|
192
240
|
signal?: AbortSignal | undefined;
|
|
193
241
|
now?: (() => number) | undefined;
|
|
@@ -206,6 +254,11 @@ export async function waitForSessionChanges(input: SessionWaitInput): Promise<Se
|
|
|
206
254
|
let liveFanout = true;
|
|
207
255
|
let waited = false;
|
|
208
256
|
const ownSessionId = input.source.readOwnPendingUpdateKinds ? input.ownSessionId : null;
|
|
257
|
+
const targetEventTypeSet: ReadonlySet<string> = new Set(
|
|
258
|
+
input.targetEventTypes ?? SESSION_WAIT_EVENT_TYPES,
|
|
259
|
+
);
|
|
260
|
+
const targetEventMatches = (event: SessionEvent): boolean =>
|
|
261
|
+
targetEventTypeSet.has(event.type) && (input.targetEventMatches?.(event) ?? true);
|
|
209
262
|
|
|
210
263
|
// One subscription per distinct session; a session may be both a target and
|
|
211
264
|
// the caller's own session, in which case either condition wakes the wait.
|
|
@@ -230,11 +283,7 @@ export async function waitForSessionChanges(input: SessionWaitInput): Promise<Se
|
|
|
230
283
|
const after = targetAfter.get(sessionId);
|
|
231
284
|
for (const event of events) {
|
|
232
285
|
if (event.sessionId !== sessionId) continue;
|
|
233
|
-
if (
|
|
234
|
-
after !== undefined &&
|
|
235
|
-
event.sequence > after &&
|
|
236
|
-
SESSION_WAIT_EVENT_TYPE_SET.has(event.type)
|
|
237
|
-
) {
|
|
286
|
+
if (after !== undefined && event.sequence > after && targetEventMatches(event)) {
|
|
238
287
|
return true;
|
|
239
288
|
}
|
|
240
289
|
if (sessionId === ownSessionId && event.type === SESSION_WAIT_OWN_PENDING_EVENT_TYPE) {
|
|
@@ -274,8 +323,7 @@ export async function waitForSessionChanges(input: SessionWaitInput): Promise<Se
|
|
|
274
323
|
const changed: SessionWaitTargetResult[] = [];
|
|
275
324
|
for (const { target, read } of targetReads) {
|
|
276
325
|
const events = read.events.filter(
|
|
277
|
-
(event) =>
|
|
278
|
-
event.sequence > target.afterSequence && SESSION_WAIT_EVENT_TYPE_SET.has(event.type),
|
|
326
|
+
(event) => event.sequence > target.afterSequence && targetEventMatches(event),
|
|
279
327
|
);
|
|
280
328
|
if (events.length === 0) continue;
|
|
281
329
|
changed.push({
|