@opengeni/api-router 2.5.0 → 2.6.4
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.js +1 -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-XTLI3CBH.js} +2236 -387
- package/dist/chunk-XTLI3CBH.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +104 -4
- 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/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/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 +15 -15
- package/src/app.ts +172 -20
- 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/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- 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 +318 -26
- package/src/mcp/session-wait.ts +56 -8
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- 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/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/scheduled-tasks.ts +9 -0
- package/src/routes/sessions.ts +3 -6
- package/src/routes/skills.ts +3 -3
- package/src/routes/workspaces.ts +293 -54
- 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,14 @@ import {
|
|
|
141
149
|
authorizedSocialConnectionsForGrant,
|
|
142
150
|
authorizedAtlassianConnectionsForGrant,
|
|
143
151
|
buildCapabilityCatalog,
|
|
152
|
+
correctWorkspaceMemoryWithSlackPublication,
|
|
144
153
|
nativeConnectionCapabilityRecommendations,
|
|
145
154
|
requireLiveAgentAttemptAuthorization,
|
|
146
155
|
requireSessionAuthorization,
|
|
147
156
|
requireSessionAuthorizationListScope,
|
|
148
157
|
SessionAuthorizationDeniedError,
|
|
149
158
|
SessionAuthorizationUnavailableError,
|
|
159
|
+
saveWorkspaceMemoryWithSlackPublication,
|
|
150
160
|
searchCapabilityCatalogItems,
|
|
151
161
|
type ResolvedSessionAuthorization,
|
|
152
162
|
} from "@opengeni/core";
|
|
@@ -191,6 +201,7 @@ import {
|
|
|
191
201
|
ScheduledTaskSyncError,
|
|
192
202
|
syncCreatedScheduledTask,
|
|
193
203
|
syncUpdatedScheduledTask,
|
|
204
|
+
validateScheduledTaskMachineTarget,
|
|
194
205
|
validateScheduledTaskTarget,
|
|
195
206
|
updateScheduledTaskForApi,
|
|
196
207
|
validatedScheduledTaskUpdate,
|
|
@@ -228,11 +239,13 @@ import {
|
|
|
228
239
|
SESSION_EVENT_MCP_MAX_BYTES,
|
|
229
240
|
} from "./session-view";
|
|
230
241
|
import {
|
|
242
|
+
SESSION_WAIT_COMPLETION_EVENT_TYPES,
|
|
231
243
|
SESSION_WAIT_DEFAULT_SECONDS,
|
|
232
244
|
SESSION_WAIT_EVENT_TYPES,
|
|
233
245
|
SESSION_WAIT_EVENTS_PER_TARGET,
|
|
234
246
|
SESSION_WAIT_MAX_SECONDS,
|
|
235
247
|
SESSION_WAIT_MAX_TARGETS,
|
|
248
|
+
sessionWaitCompletionEventMatches,
|
|
236
249
|
waitForSessionChanges,
|
|
237
250
|
} from "./session-wait";
|
|
238
251
|
import {
|
|
@@ -275,6 +288,12 @@ export type McpServerOptions = {
|
|
|
275
288
|
|
|
276
289
|
const ORCHESTRATION_FAILURE_CODE_MAX_LENGTH = 128;
|
|
277
290
|
const ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1_024;
|
|
291
|
+
// Keep pathological raw MCP payloads away from Unicode normalization while
|
|
292
|
+
// leaving the shared DB normalizer authoritative for the exact post-NFKC
|
|
293
|
+
// code-point limit. The multiplier admits supplementary-plane characters,
|
|
294
|
+
// decomposed forms, and ordinary whitespace folding without reopening the
|
|
295
|
+
// API-wide request-body ceiling for this 200-code-point field.
|
|
296
|
+
const MCP_DISCOVERY_QUERY_MAX_UTF16_CODE_UNITS = WORK_DISCOVERY_QUERY_MAX_CHARS * 8;
|
|
278
297
|
|
|
279
298
|
type OrchestrationToolName = "session_create" | "session_send_message";
|
|
280
299
|
|
|
@@ -409,9 +428,6 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
409
428
|
goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
410
429
|
goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
411
430
|
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
431
|
memory_save: { sessionRequired: true, allOf: ["documents:search"] },
|
|
416
432
|
memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
|
|
417
433
|
preference_registry_summary: {
|
|
@@ -1714,6 +1730,14 @@ export function buildOpenGeniMcpServer(
|
|
|
1714
1730
|
agentConfig: task.agentConfig,
|
|
1715
1731
|
missingTargetStatus: 404,
|
|
1716
1732
|
});
|
|
1733
|
+
await validateScheduledTaskMachineTarget({
|
|
1734
|
+
settings: deps.settings,
|
|
1735
|
+
db: deps.db,
|
|
1736
|
+
grant,
|
|
1737
|
+
runMode: task.runMode,
|
|
1738
|
+
agentConfig: task.agentConfig,
|
|
1739
|
+
requireOnline: true,
|
|
1740
|
+
});
|
|
1717
1741
|
await requireLimit(deps, {
|
|
1718
1742
|
accountId: grant.accountId,
|
|
1719
1743
|
workspaceId: grant.workspaceId,
|
|
@@ -3425,6 +3449,7 @@ function registerPreferenceRegistryTools(
|
|
|
3425
3449
|
}
|
|
3426
3450
|
|
|
3427
3451
|
const MemoryKindSchema = z4.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
|
|
3452
|
+
const MemoryWriteKindSchema = z4.enum(["semantic", "decision", "episodic"]);
|
|
3428
3453
|
|
|
3429
3454
|
function scheduledTaskReceipt(
|
|
3430
3455
|
operation: string,
|
|
@@ -3516,6 +3541,11 @@ export function memorySlackPublicationActor(
|
|
|
3516
3541
|
};
|
|
3517
3542
|
}
|
|
3518
3543
|
|
|
3544
|
+
function memoryPreview(text: string): string {
|
|
3545
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
3546
|
+
return normalized.length <= 120 ? normalized : `${normalized.slice(0, 119)}…`;
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3519
3549
|
function registerMemoryTools(
|
|
3520
3550
|
server: McpServer,
|
|
3521
3551
|
deps: ApiRouteDeps,
|
|
@@ -3524,10 +3554,17 @@ function registerMemoryTools(
|
|
|
3524
3554
|
json: JsonResult,
|
|
3525
3555
|
promptMode: WorkspaceMemoryPromptMode,
|
|
3526
3556
|
): void {
|
|
3557
|
+
const publicationInputSchema = z4.object({
|
|
3558
|
+
importance: z4.enum(["major", "normal", "minor"]),
|
|
3559
|
+
audience: z4.literal("workspace"),
|
|
3560
|
+
slackMode: z4.enum(["auto", "review", "never"]),
|
|
3561
|
+
shareSummary: z4.string().trim().min(1).max(4_096),
|
|
3562
|
+
});
|
|
3563
|
+
|
|
3527
3564
|
server.registerTool(
|
|
3528
3565
|
"memory_search",
|
|
3529
3566
|
{
|
|
3530
|
-
description: `${MEMORY_SEARCH_TOOL_DESCRIPTION} Legacy preference
|
|
3567
|
+
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
3568
|
inputSchema: {
|
|
3532
3569
|
query: z4.string().min(1),
|
|
3533
3570
|
kind: MemoryKindSchema.optional(),
|
|
@@ -3550,10 +3587,239 @@ function registerMemoryTools(
|
|
|
3550
3587
|
}),
|
|
3551
3588
|
);
|
|
3552
3589
|
|
|
3553
|
-
// Memory
|
|
3554
|
-
//
|
|
3555
|
-
|
|
3556
|
-
|
|
3590
|
+
// Memory writes are agent-only. Human creation and curation use the REST/UI
|
|
3591
|
+
// surface; a non-attempt MCP principal may search but cannot mutate Memory.
|
|
3592
|
+
if (exactAgentAttemptClaims(grant) === null) return;
|
|
3593
|
+
|
|
3594
|
+
server.registerTool(
|
|
3595
|
+
"memory_save",
|
|
3596
|
+
{
|
|
3597
|
+
description: MEMORY_SAVE_TOOL_DESCRIPTION,
|
|
3598
|
+
inputSchema: {
|
|
3599
|
+
text: z4.string().min(1),
|
|
3600
|
+
kind: MemoryWriteKindSchema,
|
|
3601
|
+
confidence: z4.number().min(0).max(1).optional(),
|
|
3602
|
+
replaces_id: z4.string().min(1).optional(),
|
|
3603
|
+
slack_publication: publicationInputSchema.optional(),
|
|
3604
|
+
},
|
|
3605
|
+
},
|
|
3606
|
+
async ({ text, kind, confidence, replaces_id, slack_publication }) => {
|
|
3607
|
+
const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
3608
|
+
const result = await saveWorkspaceMemoryWithSlackPublication(
|
|
3609
|
+
deps.db,
|
|
3610
|
+
{
|
|
3611
|
+
accountId: grant.accountId,
|
|
3612
|
+
workspaceId: grant.workspaceId,
|
|
3613
|
+
sessionId,
|
|
3614
|
+
text,
|
|
3615
|
+
kind,
|
|
3616
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
3617
|
+
...(replaces_id ? { replacesId: replaces_id } : {}),
|
|
3618
|
+
origin: "agent",
|
|
3619
|
+
},
|
|
3620
|
+
slack_publication
|
|
3621
|
+
? {
|
|
3622
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
3623
|
+
actor: memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null)
|
|
3624
|
+
.actor,
|
|
3625
|
+
ownerLabel: actor.initiator.label ?? grant.subjectLabel ?? null,
|
|
3626
|
+
}
|
|
3627
|
+
: null,
|
|
3628
|
+
deps.getDocumentServices().embedder,
|
|
3629
|
+
);
|
|
3630
|
+
let timelineWarning: string | null = null;
|
|
3631
|
+
try {
|
|
3632
|
+
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
3633
|
+
{
|
|
3634
|
+
type: "memory.saved",
|
|
3635
|
+
payload: {
|
|
3636
|
+
memoryId: result.memory.id,
|
|
3637
|
+
kind: result.memory.kind,
|
|
3638
|
+
preview: memoryPreview(result.memory.text),
|
|
3639
|
+
deduped: result.deduped,
|
|
3640
|
+
...(result.superseded ? { supersededMemoryId: result.superseded.id } : {}),
|
|
3641
|
+
},
|
|
3642
|
+
},
|
|
3643
|
+
]);
|
|
3644
|
+
} catch {
|
|
3645
|
+
timelineWarning = "Memory committed, but its session timeline event could not be recorded.";
|
|
3646
|
+
console.warn("workspace memory save: committed without session timeline event", {
|
|
3647
|
+
errorClass: "MemoryTimelineOperationError",
|
|
3648
|
+
errorCode: "memory_save_timeline_append_failed",
|
|
3649
|
+
origin: "api",
|
|
3650
|
+
workspaceId: grant.workspaceId,
|
|
3651
|
+
sessionId,
|
|
3652
|
+
memoryId: result.memory.id,
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
const changed = !result.deduped || result.updated || result.superseded !== null;
|
|
3656
|
+
const outcome =
|
|
3657
|
+
result.updated || result.superseded !== null
|
|
3658
|
+
? "updated"
|
|
3659
|
+
: result.deduped
|
|
3660
|
+
? "unchanged"
|
|
3661
|
+
: "created";
|
|
3662
|
+
return json(
|
|
3663
|
+
mcpMutationReceipt({
|
|
3664
|
+
operation: "memory_save",
|
|
3665
|
+
committed: true,
|
|
3666
|
+
outcome,
|
|
3667
|
+
changed,
|
|
3668
|
+
resource: {
|
|
3669
|
+
type: "knowledge_memory",
|
|
3670
|
+
id: result.memory.id,
|
|
3671
|
+
state: result.memory.status,
|
|
3672
|
+
},
|
|
3673
|
+
relatedResources: result.superseded
|
|
3674
|
+
? [
|
|
3675
|
+
{
|
|
3676
|
+
type: "knowledge_memory",
|
|
3677
|
+
id: result.superseded.id,
|
|
3678
|
+
state: result.superseded.status,
|
|
3679
|
+
},
|
|
3680
|
+
]
|
|
3681
|
+
: undefined,
|
|
3682
|
+
timestamp: result.memory.updatedAt,
|
|
3683
|
+
idempotency: { status: "not_supported" },
|
|
3684
|
+
warnings: [
|
|
3685
|
+
...(!result.embedded
|
|
3686
|
+
? ["Memory committed without a vector embedding; keyword search remains available."]
|
|
3687
|
+
: []),
|
|
3688
|
+
...(timelineWarning ? [timelineWarning] : []),
|
|
3689
|
+
],
|
|
3690
|
+
facts: {
|
|
3691
|
+
deduped: result.deduped,
|
|
3692
|
+
dedupeReason: result.dedupeReason,
|
|
3693
|
+
updatedInPlace: result.updated,
|
|
3694
|
+
embedded: result.embedded,
|
|
3695
|
+
slackPublicationDecision: result.slackPublication.decision?.eligible
|
|
3696
|
+
? "eligible"
|
|
3697
|
+
: (result.slackPublication.decision?.reason ?? "not_requested"),
|
|
3698
|
+
slackPublicationId:
|
|
3699
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3700
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3701
|
+
? result.slackPublication.enqueue.publication.id
|
|
3702
|
+
: null,
|
|
3703
|
+
slackPublicationState:
|
|
3704
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3705
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3706
|
+
? result.slackPublication.enqueue.publication.state
|
|
3707
|
+
: null,
|
|
3708
|
+
},
|
|
3709
|
+
}),
|
|
3710
|
+
);
|
|
3711
|
+
},
|
|
3712
|
+
);
|
|
3713
|
+
|
|
3714
|
+
server.registerTool(
|
|
3715
|
+
"memory_correct",
|
|
3716
|
+
{
|
|
3717
|
+
description: MEMORY_CORRECT_TOOL_DESCRIPTION,
|
|
3718
|
+
inputSchema: {
|
|
3719
|
+
id: z4.string().min(1),
|
|
3720
|
+
reason: z4.string().min(1).optional(),
|
|
3721
|
+
replacement_text: z4.string().min(1).optional(),
|
|
3722
|
+
slack_publication: publicationInputSchema.optional(),
|
|
3723
|
+
},
|
|
3724
|
+
},
|
|
3725
|
+
async ({ id, reason, replacement_text, slack_publication }) => {
|
|
3726
|
+
const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
3727
|
+
const result = await correctWorkspaceMemoryWithSlackPublication(
|
|
3728
|
+
deps.db,
|
|
3729
|
+
{
|
|
3730
|
+
accountId: grant.accountId,
|
|
3731
|
+
workspaceId: grant.workspaceId,
|
|
3732
|
+
sessionId,
|
|
3733
|
+
id,
|
|
3734
|
+
...(reason ? { reason } : {}),
|
|
3735
|
+
...(replacement_text ? { replacementText: replacement_text } : {}),
|
|
3736
|
+
origin: "agent",
|
|
3737
|
+
},
|
|
3738
|
+
slack_publication
|
|
3739
|
+
? {
|
|
3740
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
3741
|
+
actor: memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null)
|
|
3742
|
+
.actor,
|
|
3743
|
+
ownerLabel: actor.initiator.label ?? grant.subjectLabel ?? null,
|
|
3744
|
+
}
|
|
3745
|
+
: null,
|
|
3746
|
+
deps.getDocumentServices().embedder,
|
|
3747
|
+
);
|
|
3748
|
+
let timelineWarning: string | null = null;
|
|
3749
|
+
try {
|
|
3750
|
+
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
3751
|
+
{
|
|
3752
|
+
type: "memory.corrected",
|
|
3753
|
+
payload: {
|
|
3754
|
+
memoryId: result.memory.id,
|
|
3755
|
+
kind: result.memory.kind,
|
|
3756
|
+
preview: memoryPreview(result.memory.text),
|
|
3757
|
+
action: result.action,
|
|
3758
|
+
...(reason ? { reason: memoryPreview(reason) } : {}),
|
|
3759
|
+
...(result.replacement
|
|
3760
|
+
? {
|
|
3761
|
+
replacementMemoryId: result.replacement.id,
|
|
3762
|
+
replacementPreview: memoryPreview(result.replacement.text),
|
|
3763
|
+
}
|
|
3764
|
+
: {}),
|
|
3765
|
+
},
|
|
3766
|
+
},
|
|
3767
|
+
]);
|
|
3768
|
+
} catch {
|
|
3769
|
+
timelineWarning =
|
|
3770
|
+
"Memory correction committed, but its session timeline event could not be recorded.";
|
|
3771
|
+
console.warn("workspace memory correction: committed without session timeline event", {
|
|
3772
|
+
errorClass: "MemoryTimelineOperationError",
|
|
3773
|
+
errorCode: "memory_correct_timeline_append_failed",
|
|
3774
|
+
origin: "api",
|
|
3775
|
+
workspaceId: grant.workspaceId,
|
|
3776
|
+
sessionId,
|
|
3777
|
+
memoryId: result.memory.id,
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
return json(
|
|
3781
|
+
mcpMutationReceipt({
|
|
3782
|
+
operation: "memory_correct",
|
|
3783
|
+
committed: true,
|
|
3784
|
+
outcome: "updated",
|
|
3785
|
+
changed: true,
|
|
3786
|
+
resource: {
|
|
3787
|
+
type: "knowledge_memory",
|
|
3788
|
+
id: result.memory.id,
|
|
3789
|
+
state: result.memory.status,
|
|
3790
|
+
},
|
|
3791
|
+
relatedResources: result.replacement
|
|
3792
|
+
? [
|
|
3793
|
+
{
|
|
3794
|
+
type: "knowledge_memory",
|
|
3795
|
+
id: result.replacement.id,
|
|
3796
|
+
state: result.replacement.status,
|
|
3797
|
+
},
|
|
3798
|
+
]
|
|
3799
|
+
: undefined,
|
|
3800
|
+
timestamp: (result.replacement ?? result.memory).updatedAt,
|
|
3801
|
+
idempotency: { status: "not_supported" },
|
|
3802
|
+
warnings: timelineWarning ? [timelineWarning] : [],
|
|
3803
|
+
facts: {
|
|
3804
|
+
correctionAction: result.action,
|
|
3805
|
+
slackPublicationDecision: result.slackPublication.decision?.eligible
|
|
3806
|
+
? "eligible"
|
|
3807
|
+
: (result.slackPublication.decision?.reason ?? "not_requested"),
|
|
3808
|
+
slackPublicationId:
|
|
3809
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3810
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3811
|
+
? result.slackPublication.enqueue.publication.id
|
|
3812
|
+
: null,
|
|
3813
|
+
slackPublicationState:
|
|
3814
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3815
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3816
|
+
? result.slackPublication.enqueue.publication.state
|
|
3817
|
+
: null,
|
|
3818
|
+
},
|
|
3819
|
+
}),
|
|
3820
|
+
);
|
|
3821
|
+
},
|
|
3822
|
+
);
|
|
3557
3823
|
}
|
|
3558
3824
|
|
|
3559
3825
|
// Fleet tools (M7 bring-your-own-compute). Session-scoped (they steer THIS
|
|
@@ -4155,7 +4421,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4155
4421
|
includeLastMessage: z4.boolean().optional(),
|
|
4156
4422
|
orderBy: z4.enum(["createdAt", "updatedAt", "relevance"]).optional(),
|
|
4157
4423
|
updatedAfter: z4.string().max(64).optional(),
|
|
4158
|
-
query: z4.string().max(
|
|
4424
|
+
query: z4.string().max(MCP_DISCOVERY_QUERY_MAX_UTF16_CODE_UNITS).optional(),
|
|
4159
4425
|
statuses: z4
|
|
4160
4426
|
.array(
|
|
4161
4427
|
z4.enum([
|
|
@@ -4443,7 +4709,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4443
4709
|
server.registerTool(
|
|
4444
4710
|
"session_wait",
|
|
4445
4711
|
{
|
|
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)
|
|
4712
|
+
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
4713
|
inputSchema: {
|
|
4448
4714
|
targets: z4
|
|
4449
4715
|
.array(
|
|
@@ -4460,10 +4726,16 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4460
4726
|
.describe(
|
|
4461
4727
|
"Also return when your own session has pending machine input (default true).",
|
|
4462
4728
|
),
|
|
4729
|
+
waitFor: z4
|
|
4730
|
+
.enum(["change", "completion"])
|
|
4731
|
+
.optional()
|
|
4732
|
+
.describe(
|
|
4733
|
+
"change (default) returns on relevant activity; completion ignores messages, goal/progress, maintenance, and continuation segments until a result-bearing final turn or blocker.",
|
|
4734
|
+
),
|
|
4463
4735
|
maxWaitSeconds: z4.number().int().min(1).max(SESSION_WAIT_MAX_SECONDS).optional(),
|
|
4464
4736
|
},
|
|
4465
4737
|
},
|
|
4466
|
-
async ({ targets, includeOwnPendingUpdates, maxWaitSeconds }, extra) => {
|
|
4738
|
+
async ({ targets, includeOwnPendingUpdates, waitFor, maxWaitSeconds }, extra) => {
|
|
4467
4739
|
const distinct = new Set(targets.map((target) => target.sessionId));
|
|
4468
4740
|
if (distinct.size !== targets.length) {
|
|
4469
4741
|
throw new Error("session_wait targets must name distinct sessions");
|
|
@@ -4475,6 +4747,8 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4475
4747
|
await requireSession(deps.db, grant.workspaceId, target.sessionId);
|
|
4476
4748
|
}
|
|
4477
4749
|
const ownSessionId = includeOwnPendingUpdates === false ? null : callerSessionId;
|
|
4750
|
+
const targetEventTypes =
|
|
4751
|
+
waitFor === "completion" ? SESSION_WAIT_COMPLETION_EVENT_TYPES : SESSION_WAIT_EVENT_TYPES;
|
|
4478
4752
|
// The API serves one transport per POST, so the worker's MCP cancel
|
|
4479
4753
|
// notification never reaches this handler; the route binds the HTTP
|
|
4480
4754
|
// request's abort to transport.close() (mcp/request-abort.ts), which
|
|
@@ -4491,6 +4765,9 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4491
4765
|
targets,
|
|
4492
4766
|
ownSessionId,
|
|
4493
4767
|
maxWaitMs: (maxWaitSeconds ?? SESSION_WAIT_DEFAULT_SECONDS) * 1_000,
|
|
4768
|
+
targetEventTypes,
|
|
4769
|
+
targetEventMatches:
|
|
4770
|
+
waitFor === "completion" ? sessionWaitCompletionEventMatches : undefined,
|
|
4494
4771
|
signal,
|
|
4495
4772
|
source: {
|
|
4496
4773
|
reauthorizeTargets: async (sessionIds) => {
|
|
@@ -4509,7 +4786,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4509
4786
|
direction: "after",
|
|
4510
4787
|
limit: SESSION_WAIT_EVENTS_PER_TARGET,
|
|
4511
4788
|
payloadMode: "full",
|
|
4512
|
-
includeTypes:
|
|
4789
|
+
includeTypes: targetEventTypes,
|
|
4513
4790
|
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
|
|
4514
4791
|
});
|
|
4515
4792
|
return { events: page.events, hasMore: page.hasMore };
|
|
@@ -4538,6 +4815,14 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4538
4815
|
const sessionCreateInput = z4
|
|
4539
4816
|
.object({
|
|
4540
4817
|
initialMessage: z4.string().min(1),
|
|
4818
|
+
title: z4
|
|
4819
|
+
.string()
|
|
4820
|
+
.min(1)
|
|
4821
|
+
.max(SESSION_TITLE_MAX_CHARACTERS)
|
|
4822
|
+
.optional()
|
|
4823
|
+
.describe(
|
|
4824
|
+
"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.",
|
|
4825
|
+
),
|
|
4541
4826
|
instructions: z4.string().min(1).max(SESSION_INSTRUCTIONS_MAX_CHARACTERS).optional(),
|
|
4542
4827
|
goal: z4.unknown().optional(),
|
|
4543
4828
|
resources: z4.array(z4.unknown()).optional(),
|
|
@@ -4620,7 +4905,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4620
4905
|
"session_create",
|
|
4621
4906
|
{
|
|
4622
4907
|
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.",
|
|
4908
|
+
"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
4909
|
inputSchema: sessionCreateInput,
|
|
4625
4910
|
},
|
|
4626
4911
|
async (args) => {
|
|
@@ -4633,18 +4918,25 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4633
4918
|
if (callerSessionId !== null) {
|
|
4634
4919
|
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
4635
4920
|
}
|
|
4636
|
-
const { machineTarget, ...request } = args;
|
|
4637
|
-
const result = await createSessionForRequestWithOutcome(
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4921
|
+
const { machineTarget, title, ...request } = args;
|
|
4922
|
+
const result = await createSessionForRequestWithOutcome(
|
|
4923
|
+
deps,
|
|
4924
|
+
grant,
|
|
4925
|
+
grant.workspaceId,
|
|
4926
|
+
{
|
|
4927
|
+
...request,
|
|
4928
|
+
...(machineTarget
|
|
4929
|
+
? {
|
|
4930
|
+
targetSandboxId: machineTarget.targetSandboxId,
|
|
4931
|
+
...(machineTarget.workingDir !== undefined
|
|
4932
|
+
? { workingDir: machineTarget.workingDir }
|
|
4933
|
+
: {}),
|
|
4934
|
+
}
|
|
4935
|
+
: {}),
|
|
4936
|
+
},
|
|
4937
|
+
undefined,
|
|
4938
|
+
title === undefined ? {} : { automaticTitleCandidate: title },
|
|
4939
|
+
);
|
|
4648
4940
|
return json(sessionCreateMutationReceipt(result, Boolean(request.idempotencyKey)));
|
|
4649
4941
|
} catch (error) {
|
|
4650
4942
|
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({
|
|
@@ -148,7 +148,7 @@ export function registerApiIntegrationRoutes(
|
|
|
148
148
|
|
|
149
149
|
app.post("/v1/workspaces/:workspaceId/integrations/install", async (c) => {
|
|
150
150
|
const workspaceId = c.req.param("workspaceId");
|
|
151
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
151
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
152
152
|
const payload = InstallApiIntegrationRequest.parse(await c.req.json());
|
|
153
153
|
const resolved = await resolveForRoute({
|
|
154
154
|
deps,
|
|
@@ -261,7 +261,7 @@ export function registerApiIntegrationRoutes(
|
|
|
261
261
|
"/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey",
|
|
262
262
|
async (c) => {
|
|
263
263
|
const workspaceId = c.req.param("workspaceId");
|
|
264
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
264
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
265
265
|
const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
|
|
266
266
|
const instanceKey = decodeURIComponent(c.req.param("instanceKey"));
|
|
267
267
|
const payload = UninstallApiIntegrationRequest.parse(await c.req.json());
|