@opengeni/db 3.7.4 → 4.0.0-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/automations.d.ts +4 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-integrations.d.ts +2 -0
- package/dist/{chunk-6DPJA36C.js → chunk-2XEZK4LF.js} +7 -1
- package/dist/chunk-2XEZK4LF.js.map +1 -0
- package/dist/{chunk-4NAR7FBA.js → chunk-3RI33YQN.js} +67 -1
- package/dist/chunk-3RI33YQN.js.map +1 -0
- package/dist/{chunk-SZAWDXQC.js → chunk-CJROYHSC.js} +3 -3
- package/dist/{chunk-DZCYW2WT.js → chunk-MX6V5F65.js} +2 -2
- package/dist/{chunk-S3TDDCS2.js → chunk-N5VXJ4I2.js} +151 -78
- package/dist/chunk-N5VXJ4I2.js.map +1 -0
- package/dist/{chunk-U4WPPEBH.js → chunk-NLFHMPUB.js} +2 -2
- package/dist/{chunk-5NNCGFQY.js → chunk-P6NSEG2S.js} +2 -2
- package/dist/{chunk-O56DM5GZ.js → chunk-QM6YOWHZ.js} +624 -57
- package/dist/chunk-QM6YOWHZ.js.map +1 -0
- package/dist/{chunk-US4IS6IV.js → chunk-RZNOSEMC.js} +2 -2
- package/dist/{chunk-F6PBS6WE.js → chunk-UFAMOUWL.js} +2 -2
- package/dist/codemode-operations.d.ts +5 -0
- package/dist/codex-token-resolver.d.ts +6 -2
- package/dist/connection-token-resolver.d.ts +8 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/generated-images.d.ts +15 -0
- package/dist/index.d.ts +393 -91
- package/dist/index.js +9992 -6069
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +9 -0
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/managed-user-setup.d.ts +3 -0
- package/dist/mcp-oauth.d.ts +80 -0
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +3 -1
- package/dist/model-catalog.d.ts +153 -0
- package/dist/model-context-snapshots.d.ts +20 -0
- package/dist/new-session-drafts.d.ts +12 -0
- package/dist/organization-membership-lifecycle.d.ts +8 -1
- package/dist/organization-model-providers.d.ts +101 -0
- package/dist/pack-components.d.ts +1 -0
- package/dist/pr-review.d.ts +9 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +8 -8
- package/dist/schema.d.ts +7708 -5050
- package/dist/schema.js +25 -1
- package/dist/session-background-commands.d.ts +18 -6
- package/dist/session-background-commands.js +15 -9
- package/dist/session-control.d.ts +37 -2
- package/dist/session-goal-pacing.d.ts +3 -6
- package/dist/session-queue-commands.d.ts +17 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/tool-gateway-approvals.d.ts +31 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-artifacts.d.ts +17 -3
- package/dist/xai-subscription.d.ts +8 -0
- package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
- package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
- package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
- package/drizzle/0390_organization_model_provider_connections.sql +197 -0
- package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
- package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
- package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
- package/drizzle/0394_session_selected_skill_activation.sql +20 -0
- package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
- package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
- package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
- package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
- package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
- package/drizzle/0400_session_attempt_model_context_snapshots.sql +65 -0
- package/drizzle/0401_organization_user_setup_token_transport.sql +178 -0
- package/drizzle/0402_session_input_wait_and_background_command_results.sql +1023 -0
- package/drizzle/0403_codex_unconditional_credential_leasing.sql +181 -0
- package/drizzle/0404_mcp_oauth_authorization_server.sql +471 -0
- package/drizzle/0405_tool_gateway_approval_capabilities.sql +184 -0
- package/drizzle/0406_workspace_html_site_sources.sql +71 -0
- package/package.json +6 -6
- package/src/automations.ts +9 -1
- package/src/capability-integrations.ts +8 -0
- package/src/codemode-operations.ts +31 -21
- package/src/codex-token-resolver.ts +16 -6
- package/src/connection-token-resolver.ts +52 -3
- package/src/generated-images.ts +70 -6
- package/src/index.ts +3894 -994
- package/src/insights-model-bundle.ts +54 -5
- package/src/insights.ts +53 -7
- package/src/managed-user-setup.ts +14 -3
- package/src/mcp-oauth.ts +488 -0
- package/src/migrate.ts +9 -1
- package/src/model-catalog.ts +682 -0
- package/src/model-context-snapshots.ts +151 -0
- package/src/new-session-drafts.ts +66 -21
- package/src/organization-membership-lifecycle.ts +47 -5
- package/src/organization-model-providers.ts +614 -0
- package/src/pack-components.ts +15 -2
- package/src/pr-review.ts +19 -0
- package/src/provision-roles.ts +4 -0
- package/src/runtime-posture.ts +64 -0
- package/src/schema.ts +666 -61
- package/src/session-background-commands.ts +180 -78
- package/src/session-control.ts +181 -12
- package/src/session-goal-pacing.ts +4 -8
- package/src/session-queue-commands.ts +157 -82
- package/src/tool-gateway-approvals.ts +168 -0
- package/src/workspace-artifacts.ts +633 -178
- package/src/xai-subscription.ts +66 -14
- package/dist/chunk-4NAR7FBA.js.map +0 -1
- package/dist/chunk-6DPJA36C.js.map +0 -1
- package/dist/chunk-O56DM5GZ.js.map +0 -1
- package/dist/chunk-S3TDDCS2.js.map +0 -1
- /package/dist/{chunk-SZAWDXQC.js.map → chunk-CJROYHSC.js.map} +0 -0
- /package/dist/{chunk-DZCYW2WT.js.map → chunk-MX6V5F65.js.map} +0 -0
- /package/dist/{chunk-U4WPPEBH.js.map → chunk-NLFHMPUB.js.map} +0 -0
- /package/dist/{chunk-5NNCGFQY.js.map → chunk-P6NSEG2S.js.map} +0 -0
- /package/dist/{chunk-US4IS6IV.js.map → chunk-RZNOSEMC.js.map} +0 -0
- /package/dist/{chunk-F6PBS6WE.js.map → chunk-UFAMOUWL.js.map} +0 -0
|
@@ -4,8 +4,8 @@ import type {
|
|
|
4
4
|
} from "@opengeni/contracts";
|
|
5
5
|
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
|
6
6
|
|
|
7
|
-
import type { Database } from "./database";
|
|
8
|
-
import { withRlsContext } from "./database";
|
|
7
|
+
import type { Database, SessionActivityDatabase } from "./database";
|
|
8
|
+
import { withRlsContext, withSessionActivityRlsContext } from "./database";
|
|
9
9
|
import * as schema from "./schema";
|
|
10
10
|
|
|
11
11
|
export type ConnectedMachineBackgroundCommandProof = {
|
|
@@ -15,6 +15,27 @@ export type ConnectedMachineBackgroundCommandProof = {
|
|
|
15
15
|
observedAt: Date;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
export const SESSION_BACKGROUND_COMMAND_REASON_MAX_BYTES = 512;
|
|
19
|
+
|
|
20
|
+
export function boundedSessionBackgroundCommandReason(value: string, label: string): string {
|
|
21
|
+
const normalized = value.trim();
|
|
22
|
+
let bounded = "";
|
|
23
|
+
let bytes = 0;
|
|
24
|
+
for (const character of normalized) {
|
|
25
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
26
|
+
if (bytes + characterBytes > SESSION_BACKGROUND_COMMAND_REASON_MAX_BYTES) break;
|
|
27
|
+
bounded += character;
|
|
28
|
+
bytes += characterBytes;
|
|
29
|
+
}
|
|
30
|
+
if (!bounded) throw new Error(`${label} must not be empty`);
|
|
31
|
+
return bounded;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SessionBackgroundCommandTerminalMutation = {
|
|
35
|
+
prepare: (tx: SessionActivityDatabase) => Promise<void>;
|
|
36
|
+
commit: (tx: SessionActivityDatabase, command: SessionBackgroundCommand) => Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
|
|
18
39
|
export type ConnectedMachineBackgroundCommandClaim = {
|
|
19
40
|
commandId: string;
|
|
20
41
|
accountId: string;
|
|
@@ -360,8 +381,10 @@ export async function recordConnectedMachineBackgroundCommandProof(
|
|
|
360
381
|
proof: ConnectedMachineBackgroundCommandProof;
|
|
361
382
|
},
|
|
362
383
|
): Promise<void> {
|
|
363
|
-
const reason =
|
|
364
|
-
|
|
384
|
+
const reason = boundedSessionBackgroundCommandReason(
|
|
385
|
+
input.proof.reason,
|
|
386
|
+
"Connected command proof reason",
|
|
387
|
+
);
|
|
365
388
|
if (input.proof.outcome === "exited" && input.proof.exitCode === null) {
|
|
366
389
|
throw new Error("Connected command exit proof requires an exit code");
|
|
367
390
|
}
|
|
@@ -444,44 +467,54 @@ export async function deferConnectedMachineBackgroundCommandReconciliation(
|
|
|
444
467
|
}
|
|
445
468
|
|
|
446
469
|
/** Settle only from the proof already checkpointed under this exact claim. */
|
|
447
|
-
export async function
|
|
470
|
+
export async function settleClaimedConnectedMachineBackgroundCommandWithMutation(
|
|
448
471
|
db: Database,
|
|
449
472
|
input: { claim: ConnectedMachineBackgroundCommandClaim },
|
|
473
|
+
mutateTerminal: SessionBackgroundCommandTerminalMutation,
|
|
450
474
|
): Promise<boolean> {
|
|
451
|
-
return await
|
|
475
|
+
return await withSessionActivityRlsContext(
|
|
452
476
|
db,
|
|
453
477
|
{ accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
|
|
454
|
-
async (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
478
|
+
async (tx) => {
|
|
479
|
+
await mutateTerminal.prepare(tx);
|
|
480
|
+
const rows = await tx.execute<{ id: string }>(sql`
|
|
481
|
+
update ${schema.sessionBackgroundCommands} command set
|
|
482
|
+
state = command.reconcile_proof_outcome,
|
|
483
|
+
exit_code = case
|
|
484
|
+
when command.reconcile_proof_outcome = 'exited'
|
|
485
|
+
then command.reconcile_proof_exit_code
|
|
486
|
+
else null
|
|
487
|
+
end,
|
|
488
|
+
settlement_reason = command.reconcile_proof_reason,
|
|
489
|
+
settled_at = command.reconcile_proof_observed_at,
|
|
490
|
+
reconcile_claim_id = null,
|
|
491
|
+
reconcile_claimed_at = null,
|
|
492
|
+
last_reconcile_outcome = 'settled_' || command.reconcile_proof_outcome,
|
|
493
|
+
updated_at = clock_timestamp()
|
|
494
|
+
where command.id = ${input.claim.commandId}
|
|
495
|
+
and command.account_id = ${input.claim.accountId}
|
|
496
|
+
and command.workspace_id = ${input.claim.workspaceId}
|
|
497
|
+
and command.session_id = ${input.claim.sessionId}
|
|
498
|
+
and command.provider = 'connected_machine'
|
|
499
|
+
and command.control_workspace_id = ${input.claim.controlWorkspaceId}
|
|
500
|
+
and command.enrollment_id = ${input.claim.enrollmentId}
|
|
501
|
+
and command.connection_instance_id = ${input.claim.connectionInstanceId}
|
|
502
|
+
and command.op_id = ${input.claim.opId}
|
|
503
|
+
and command.reconcile_claim_id = ${input.claim.claimId}
|
|
504
|
+
and command.state in ('running', 'stopping')
|
|
505
|
+
and command.reconcile_proof_outcome in ('exited', 'lost')
|
|
506
|
+
and command.reconcile_proof_observed_at is not null
|
|
507
|
+
returning command.id
|
|
508
|
+
`);
|
|
509
|
+
if (rows.length !== 1) return false;
|
|
510
|
+
const [row] = await tx
|
|
511
|
+
.select()
|
|
512
|
+
.from(schema.sessionBackgroundCommands)
|
|
513
|
+
.where(eq(schema.sessionBackgroundCommands.id, input.claim.commandId))
|
|
514
|
+
.limit(1);
|
|
515
|
+
if (!row) throw new Error("Settled Connected Machine command disappeared");
|
|
516
|
+
await mutateTerminal.commit(tx, mapCommand(row));
|
|
517
|
+
return true;
|
|
485
518
|
},
|
|
486
519
|
);
|
|
487
520
|
}
|
|
@@ -652,8 +685,8 @@ export async function requestWorkspaceBackgroundCommandCancellationsInTransactio
|
|
|
652
685
|
return Number(rows[0]?.commandCount ?? 0);
|
|
653
686
|
}
|
|
654
687
|
|
|
655
|
-
export async function
|
|
656
|
-
|
|
688
|
+
export async function settleSessionBackgroundCommandForRetainedProcessInTransaction(
|
|
689
|
+
tx: SessionActivityDatabase,
|
|
657
690
|
input: {
|
|
658
691
|
accountId: string;
|
|
659
692
|
workspaceId: string;
|
|
@@ -663,38 +696,76 @@ export async function settleSessionBackgroundCommandForRetainedProcess(
|
|
|
663
696
|
exitCode: number | null;
|
|
664
697
|
reason: string;
|
|
665
698
|
},
|
|
699
|
+
mutateTerminal: SessionBackgroundCommandTerminalMutation,
|
|
666
700
|
): Promise<SessionBackgroundCommand | null> {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
async (scopedDb) => {
|
|
671
|
-
const [row] = await scopedDb
|
|
672
|
-
.update(schema.sessionBackgroundCommands)
|
|
673
|
-
.set({
|
|
674
|
-
state: input.outcome,
|
|
675
|
-
exitCode: input.outcome === "exited" ? input.exitCode : null,
|
|
676
|
-
settlementReason: input.reason.slice(0, 512),
|
|
677
|
-
settledAt: new Date(),
|
|
678
|
-
reconcileClaimId: null,
|
|
679
|
-
reconcileClaimedAt: null,
|
|
680
|
-
lastReconcileOutcome: `settled_${input.outcome}`,
|
|
681
|
-
updatedAt: new Date(),
|
|
682
|
-
})
|
|
683
|
-
.where(
|
|
684
|
-
and(
|
|
685
|
-
eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
686
|
-
eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
|
|
687
|
-
eq(schema.sessionBackgroundCommands.retainedProcessId, input.retainedProcessId),
|
|
688
|
-
inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
|
|
689
|
-
),
|
|
690
|
-
)
|
|
691
|
-
.returning();
|
|
692
|
-
return row ? mapCommand(row) : null;
|
|
693
|
-
},
|
|
701
|
+
const reason = boundedSessionBackgroundCommandReason(
|
|
702
|
+
input.reason,
|
|
703
|
+
"Managed command settlement reason",
|
|
694
704
|
);
|
|
705
|
+
await mutateTerminal.prepare(tx);
|
|
706
|
+
const [updatedRow] = await tx
|
|
707
|
+
.update(schema.sessionBackgroundCommands)
|
|
708
|
+
.set({
|
|
709
|
+
state: input.outcome,
|
|
710
|
+
exitCode: input.outcome === "exited" ? input.exitCode : null,
|
|
711
|
+
settlementReason: reason,
|
|
712
|
+
settledAt: new Date(),
|
|
713
|
+
reconcileClaimId: null,
|
|
714
|
+
reconcileClaimedAt: null,
|
|
715
|
+
lastReconcileOutcome: `settled_${input.outcome}`,
|
|
716
|
+
updatedAt: new Date(),
|
|
717
|
+
})
|
|
718
|
+
.where(
|
|
719
|
+
and(
|
|
720
|
+
eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
721
|
+
eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
|
|
722
|
+
eq(schema.sessionBackgroundCommands.retainedProcessId, input.retainedProcessId),
|
|
723
|
+
inArray(schema.sessionBackgroundCommands.state, ["running", "stopping"]),
|
|
724
|
+
),
|
|
725
|
+
)
|
|
726
|
+
.returning();
|
|
727
|
+
let row = updatedRow;
|
|
728
|
+
if (!row) {
|
|
729
|
+
const [current] = await tx
|
|
730
|
+
.select()
|
|
731
|
+
.from(schema.sessionBackgroundCommands)
|
|
732
|
+
.where(
|
|
733
|
+
and(
|
|
734
|
+
eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
735
|
+
eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
|
|
736
|
+
eq(schema.sessionBackgroundCommands.retainedProcessId, input.retainedProcessId),
|
|
737
|
+
),
|
|
738
|
+
)
|
|
739
|
+
.for("update")
|
|
740
|
+
.limit(1);
|
|
741
|
+
if (!current) return null;
|
|
742
|
+
if (
|
|
743
|
+
current.state !== input.outcome ||
|
|
744
|
+
current.exitCode !== (input.outcome === "exited" ? input.exitCode : null)
|
|
745
|
+
) {
|
|
746
|
+
throw new Error("Managed background command terminal state conflicts with process proof");
|
|
747
|
+
}
|
|
748
|
+
const [existingEvent] = await tx
|
|
749
|
+
.select({ id: schema.sessionEvents.id })
|
|
750
|
+
.from(schema.sessionEvents)
|
|
751
|
+
.where(
|
|
752
|
+
and(
|
|
753
|
+
eq(schema.sessionEvents.workspaceId, input.workspaceId),
|
|
754
|
+
eq(schema.sessionEvents.sessionId, input.sessionId),
|
|
755
|
+
eq(schema.sessionEvents.type, "session.command.finished"),
|
|
756
|
+
sql`${schema.sessionEvents.payload} ->> 'commandId' = ${current.id}`,
|
|
757
|
+
),
|
|
758
|
+
)
|
|
759
|
+
.limit(1);
|
|
760
|
+
if (existingEvent) return mapCommand(current);
|
|
761
|
+
row = current;
|
|
762
|
+
}
|
|
763
|
+
const command = mapCommand(row);
|
|
764
|
+
await mutateTerminal.commit(tx, command);
|
|
765
|
+
return command;
|
|
695
766
|
}
|
|
696
767
|
|
|
697
|
-
export async function
|
|
768
|
+
export async function settleConnectedMachineSessionBackgroundCommandWithMutation(
|
|
698
769
|
db: Database,
|
|
699
770
|
input: {
|
|
700
771
|
accountId: string;
|
|
@@ -709,21 +780,25 @@ export async function settleConnectedMachineSessionBackgroundCommand(
|
|
|
709
780
|
exitCode: number | null;
|
|
710
781
|
reason: string;
|
|
711
782
|
},
|
|
783
|
+
mutateTerminal: SessionBackgroundCommandTerminalMutation,
|
|
712
784
|
): Promise<SessionBackgroundCommand | null> {
|
|
713
|
-
|
|
785
|
+
const reason = boundedSessionBackgroundCommandReason(
|
|
786
|
+
input.reason,
|
|
787
|
+
"Connected command settlement reason",
|
|
788
|
+
);
|
|
789
|
+
if (input.outcome === "exited" && input.exitCode === null) {
|
|
790
|
+
throw new Error("Connected command exit settlement requires an exit code");
|
|
791
|
+
}
|
|
792
|
+
if (input.outcome === "lost" && input.exitCode !== null) {
|
|
793
|
+
throw new Error("Connected command loss settlement cannot carry an exit code");
|
|
794
|
+
}
|
|
795
|
+
return await withSessionActivityRlsContext(
|
|
714
796
|
db,
|
|
715
797
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
716
|
-
async (
|
|
798
|
+
async (tx) => {
|
|
799
|
+
await mutateTerminal.prepare(tx);
|
|
717
800
|
const settledAt = new Date();
|
|
718
|
-
const
|
|
719
|
-
if (!reason) throw new Error("Connected command settlement reason must not be empty");
|
|
720
|
-
if (input.outcome === "exited" && input.exitCode === null) {
|
|
721
|
-
throw new Error("Connected command exit settlement requires an exit code");
|
|
722
|
-
}
|
|
723
|
-
if (input.outcome === "lost" && input.exitCode !== null) {
|
|
724
|
-
throw new Error("Connected command loss settlement cannot carry an exit code");
|
|
725
|
-
}
|
|
726
|
-
const [row] = await scopedDb
|
|
801
|
+
const [row] = await tx
|
|
727
802
|
.update(schema.sessionBackgroundCommands)
|
|
728
803
|
.set({
|
|
729
804
|
state: input.outcome,
|
|
@@ -753,6 +828,33 @@ export async function settleConnectedMachineSessionBackgroundCommand(
|
|
|
753
828
|
),
|
|
754
829
|
)
|
|
755
830
|
.returning();
|
|
831
|
+
if (!row) return null;
|
|
832
|
+
const command = mapCommand(row);
|
|
833
|
+
await mutateTerminal.commit(tx, command);
|
|
834
|
+
return command;
|
|
835
|
+
},
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export async function getSessionBackgroundCommand(
|
|
840
|
+
db: Database,
|
|
841
|
+
input: { accountId: string; workspaceId: string; sessionId: string; commandId: string },
|
|
842
|
+
): Promise<SessionBackgroundCommand | null> {
|
|
843
|
+
return await withRlsContext(
|
|
844
|
+
db,
|
|
845
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
846
|
+
async (scopedDb) => {
|
|
847
|
+
const [row] = await scopedDb
|
|
848
|
+
.select()
|
|
849
|
+
.from(schema.sessionBackgroundCommands)
|
|
850
|
+
.where(
|
|
851
|
+
and(
|
|
852
|
+
eq(schema.sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
853
|
+
eq(schema.sessionBackgroundCommands.sessionId, input.sessionId),
|
|
854
|
+
eq(schema.sessionBackgroundCommands.id, input.commandId),
|
|
855
|
+
),
|
|
856
|
+
)
|
|
857
|
+
.limit(1);
|
|
756
858
|
return row ? mapCommand(row) : null;
|
|
757
859
|
},
|
|
758
860
|
);
|
package/src/session-control.ts
CHANGED
|
@@ -244,11 +244,11 @@ export async function assertAgentCommandAuthorityInTransaction(
|
|
|
244
244
|
workspaceId: string;
|
|
245
245
|
actor: Extract<SessionCommandActor, { type: "agent_attempt" }>;
|
|
246
246
|
targetSessionId: string;
|
|
247
|
-
action: "pause" | "resume" | "steer" | "message" | "goal";
|
|
247
|
+
action: "pause" | "resume" | "steer" | "message" | "goal" | "wait";
|
|
248
248
|
},
|
|
249
249
|
): Promise<void> {
|
|
250
|
-
if (input.action
|
|
251
|
-
throw new SessionControlInvariantError("An agent
|
|
250
|
+
if (["goal", "wait"].includes(input.action) && input.targetSessionId !== input.actor.sessionId) {
|
|
251
|
+
throw new SessionControlInvariantError("An agent self command must target its own session");
|
|
252
252
|
}
|
|
253
253
|
// Every command caller establishes the control/workspace prefix first.
|
|
254
254
|
// Reusing the event-write helper here keeps cross-session actor authority on
|
|
@@ -1855,13 +1855,13 @@ async function findCommandReceipt(
|
|
|
1855
1855
|
targetSessionId: string | null;
|
|
1856
1856
|
targetTurnId: string | null;
|
|
1857
1857
|
operationKey: string;
|
|
1858
|
-
identityScope: "actor" | "
|
|
1858
|
+
identityScope: "actor" | "target_operation";
|
|
1859
1859
|
},
|
|
1860
1860
|
): Promise<SessionCommandReceiptRow | null> {
|
|
1861
1861
|
const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
|
|
1862
1862
|
const actorAttemptId = input.actor.type === "agent_attempt" ? input.actor.attemptId : null;
|
|
1863
1863
|
const identity =
|
|
1864
|
-
input.identityScope === "
|
|
1864
|
+
input.identityScope === "target_operation"
|
|
1865
1865
|
? and(
|
|
1866
1866
|
eq(schema.sessionCommandReceipts.workspaceId, input.workspaceId),
|
|
1867
1867
|
eq(schema.sessionCommandReceipts.actorType, "agent_attempt"),
|
|
@@ -1884,6 +1884,174 @@ async function findCommandReceipt(
|
|
|
1884
1884
|
return rows[0] ?? null;
|
|
1885
1885
|
}
|
|
1886
1886
|
|
|
1887
|
+
export type SessionPromptCommandAction = "prompt.send" | "prompt.steer";
|
|
1888
|
+
|
|
1889
|
+
async function findPromptCommandReceipts(
|
|
1890
|
+
db: Database,
|
|
1891
|
+
input: {
|
|
1892
|
+
workspaceId: string;
|
|
1893
|
+
actor: SessionCommandActor;
|
|
1894
|
+
operationKey: string;
|
|
1895
|
+
lock?: "none" | "update";
|
|
1896
|
+
},
|
|
1897
|
+
): Promise<SessionCommandReceiptRow[]> {
|
|
1898
|
+
const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
|
|
1899
|
+
const actorAttemptId = input.actor.type === "agent_attempt" ? input.actor.attemptId : null;
|
|
1900
|
+
const query = db
|
|
1901
|
+
.select()
|
|
1902
|
+
.from(schema.sessionCommandReceipts)
|
|
1903
|
+
.where(
|
|
1904
|
+
and(
|
|
1905
|
+
eq(schema.sessionCommandReceipts.workspaceId, input.workspaceId),
|
|
1906
|
+
eq(schema.sessionCommandReceipts.actorType, input.actor.type),
|
|
1907
|
+
sql`${schema.sessionCommandReceipts.actorSubjectId} is not distinct from ${actorSubjectId}`,
|
|
1908
|
+
sql`${schema.sessionCommandReceipts.actorAttemptId} is not distinct from ${actorAttemptId}::uuid`,
|
|
1909
|
+
inArray(schema.sessionCommandReceipts.action, ["prompt.send", "prompt.steer"]),
|
|
1910
|
+
eq(schema.sessionCommandReceipts.operationKey, input.operationKey),
|
|
1911
|
+
),
|
|
1912
|
+
);
|
|
1913
|
+
const rows = input.lock === "update" ? await query.for("update") : await query;
|
|
1914
|
+
return rows;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function fingerprintedPromptCommandReceipt(
|
|
1918
|
+
receipts: readonly SessionCommandReceiptRow[],
|
|
1919
|
+
): SessionCommandReceiptRow | null {
|
|
1920
|
+
const fingerprinted = receipts.filter(
|
|
1921
|
+
(receipt) => typeof receipt.result.boundaryRequestHash === "string",
|
|
1922
|
+
);
|
|
1923
|
+
if (fingerprinted.length > 1) {
|
|
1924
|
+
throw new SessionControlInvariantError(
|
|
1925
|
+
"Prompt operation key resolved to multiple fingerprinted receipts",
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
return fingerprinted[0] ?? null;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
function assertPromptCommandReceiptIdentity(
|
|
1932
|
+
receipt: SessionCommandReceiptRow,
|
|
1933
|
+
input: {
|
|
1934
|
+
action: SessionPromptCommandAction;
|
|
1935
|
+
targetSessionId: string;
|
|
1936
|
+
boundaryRequestHash: string;
|
|
1937
|
+
},
|
|
1938
|
+
): void {
|
|
1939
|
+
if (
|
|
1940
|
+
receipt.action !== input.action ||
|
|
1941
|
+
receipt.targetSessionId !== input.targetSessionId ||
|
|
1942
|
+
receipt.targetTurnId !== null ||
|
|
1943
|
+
(typeof receipt.result.boundaryRequestHash === "string" &&
|
|
1944
|
+
receipt.result.boundaryRequestHash !== input.boundaryRequestHash)
|
|
1945
|
+
) {
|
|
1946
|
+
throw new SessionCommandIdempotencyError();
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/**
|
|
1951
|
+
* Prompt operation keys are actor-scoped across target sessions and across the
|
|
1952
|
+
* Send/Steer action pair. The advisory lock closes the gap left by the legacy
|
|
1953
|
+
* action/target-specific unique index without changing old receipt identities.
|
|
1954
|
+
*/
|
|
1955
|
+
export async function reserveSessionPromptCommandReceipt(
|
|
1956
|
+
db: Database,
|
|
1957
|
+
input: {
|
|
1958
|
+
accountId: string;
|
|
1959
|
+
workspaceId: string;
|
|
1960
|
+
actor: SessionCommandActor;
|
|
1961
|
+
action: SessionPromptCommandAction;
|
|
1962
|
+
targetSessionId: string;
|
|
1963
|
+
operationKey: string;
|
|
1964
|
+
canonicalRequestHash: string;
|
|
1965
|
+
boundaryRequestHash: string;
|
|
1966
|
+
},
|
|
1967
|
+
): Promise<{ receipt: SessionCommandReceiptRow; replay: boolean }> {
|
|
1968
|
+
await lockSessionPromptCommandOperation(db, input);
|
|
1969
|
+
const existingReceipts = await findPromptCommandReceipts(db, {
|
|
1970
|
+
workspaceId: input.workspaceId,
|
|
1971
|
+
actor: input.actor,
|
|
1972
|
+
operationKey: input.operationKey,
|
|
1973
|
+
lock: "update",
|
|
1974
|
+
});
|
|
1975
|
+
const fingerprinted = fingerprintedPromptCommandReceipt(existingReceipts);
|
|
1976
|
+
if (fingerprinted) {
|
|
1977
|
+
assertPromptCommandReceiptIdentity(fingerprinted, input);
|
|
1978
|
+
return { receipt: fingerprinted, replay: true };
|
|
1979
|
+
}
|
|
1980
|
+
const legacyExact = existingReceipts.find(
|
|
1981
|
+
(receipt) =>
|
|
1982
|
+
receipt.action === input.action &&
|
|
1983
|
+
receipt.targetSessionId === input.targetSessionId &&
|
|
1984
|
+
receipt.targetTurnId === null,
|
|
1985
|
+
);
|
|
1986
|
+
if (legacyExact) {
|
|
1987
|
+
if (legacyExact.canonicalRequestHash !== input.canonicalRequestHash) {
|
|
1988
|
+
throw new SessionCommandIdempotencyError();
|
|
1989
|
+
}
|
|
1990
|
+
return { receipt: legacyExact, replay: true };
|
|
1991
|
+
}
|
|
1992
|
+
return await reserveSessionCommandReceipt(db, {
|
|
1993
|
+
accountId: input.accountId,
|
|
1994
|
+
workspaceId: input.workspaceId,
|
|
1995
|
+
actor: input.actor,
|
|
1996
|
+
action: input.action,
|
|
1997
|
+
targetSessionId: input.targetSessionId,
|
|
1998
|
+
targetTurnId: null,
|
|
1999
|
+
operationKey: input.operationKey,
|
|
2000
|
+
canonicalRequestHash: input.canonicalRequestHash,
|
|
2001
|
+
...(existingReceipts.length === 0
|
|
2002
|
+
? { initialResult: { boundaryRequestHash: input.boundaryRequestHash } }
|
|
2003
|
+
: {}),
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/** Serialize one actor-scoped prompt operation before replay or reservation. */
|
|
2008
|
+
export async function lockSessionPromptCommandOperation(
|
|
2009
|
+
db: Database,
|
|
2010
|
+
input: {
|
|
2011
|
+
workspaceId: string;
|
|
2012
|
+
actor: SessionCommandActor;
|
|
2013
|
+
operationKey: string;
|
|
2014
|
+
},
|
|
2015
|
+
): Promise<void> {
|
|
2016
|
+
const actorIdentity =
|
|
2017
|
+
input.actor.type === "agent_attempt"
|
|
2018
|
+
? `attempt:${input.actor.attemptId}`
|
|
2019
|
+
: `${input.actor.type}:${input.actor.subjectId}`;
|
|
2020
|
+
await db.execute(
|
|
2021
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`session-prompt:${input.workspaceId}:${actorIdentity}:${input.operationKey}`}, 0))`,
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/** Return only a fully committed exact prompt receipt with the new boundary fingerprint. */
|
|
2026
|
+
export async function getCompletedSessionPromptCommandReceipt(
|
|
2027
|
+
db: Database,
|
|
2028
|
+
input: {
|
|
2029
|
+
workspaceId: string;
|
|
2030
|
+
actor: SessionCommandActor;
|
|
2031
|
+
action: SessionPromptCommandAction;
|
|
2032
|
+
targetSessionId: string;
|
|
2033
|
+
operationKey: string;
|
|
2034
|
+
boundaryRequestHash: string;
|
|
2035
|
+
},
|
|
2036
|
+
): Promise<SessionCommandReceiptRow | null> {
|
|
2037
|
+
const receipt = fingerprintedPromptCommandReceipt(
|
|
2038
|
+
await findPromptCommandReceipts(db, {
|
|
2039
|
+
workspaceId: input.workspaceId,
|
|
2040
|
+
actor: input.actor,
|
|
2041
|
+
operationKey: input.operationKey,
|
|
2042
|
+
}),
|
|
2043
|
+
);
|
|
2044
|
+
if (!receipt) return null;
|
|
2045
|
+
assertPromptCommandReceiptIdentity(receipt, input);
|
|
2046
|
+
if (
|
|
2047
|
+
typeof receipt.result.boundaryRequestHash !== "string" ||
|
|
2048
|
+
receipt.appliedQueueVersion === null
|
|
2049
|
+
) {
|
|
2050
|
+
return null;
|
|
2051
|
+
}
|
|
2052
|
+
return receipt;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1887
2055
|
export async function reserveSessionCommandReceipt(
|
|
1888
2056
|
db: Database,
|
|
1889
2057
|
input: {
|
|
@@ -1895,20 +2063,21 @@ export async function reserveSessionCommandReceipt(
|
|
|
1895
2063
|
targetTurnId: string | null;
|
|
1896
2064
|
operationKey: string;
|
|
1897
2065
|
canonicalRequestHash: string;
|
|
1898
|
-
identityScope?: "actor" | "
|
|
2066
|
+
identityScope?: "actor" | "target_operation";
|
|
2067
|
+
initialResult?: Record<string, unknown>;
|
|
1899
2068
|
},
|
|
1900
2069
|
): Promise<{ receipt: SessionCommandReceiptRow; replay: boolean }> {
|
|
1901
2070
|
if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
|
|
1902
2071
|
const identityScope = input.identityScope ?? "actor";
|
|
1903
2072
|
if (
|
|
1904
|
-
identityScope === "
|
|
2073
|
+
identityScope === "target_operation" &&
|
|
1905
2074
|
(input.actor.type !== "agent_attempt" ||
|
|
1906
|
-
!["goal.update", "goal.progress", "
|
|
2075
|
+
!["goal.update", "goal.progress", "session.wait_for_input"].includes(input.action) ||
|
|
1907
2076
|
input.targetSessionId === null ||
|
|
1908
2077
|
input.targetTurnId !== null)
|
|
1909
2078
|
) {
|
|
1910
2079
|
throw new SessionControlInvariantError(
|
|
1911
|
-
"Target-scoped receipt identity is reserved for agent
|
|
2080
|
+
"Target-scoped receipt identity is reserved for agent self commands",
|
|
1912
2081
|
);
|
|
1913
2082
|
}
|
|
1914
2083
|
const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
|
|
@@ -1926,6 +2095,7 @@ export async function reserveSessionCommandReceipt(
|
|
|
1926
2095
|
targetTurnId: input.targetTurnId,
|
|
1927
2096
|
operationKey: input.operationKey,
|
|
1928
2097
|
canonicalRequestHash: input.canonicalRequestHash,
|
|
2098
|
+
result: input.initialResult ?? {},
|
|
1929
2099
|
})
|
|
1930
2100
|
.onConflictDoNothing()
|
|
1931
2101
|
.returning();
|
|
@@ -1990,13 +2160,12 @@ async function registerContinuableWakes(
|
|
|
1990
2160
|
)
|
|
1991
2161
|
), upserted as (
|
|
1992
2162
|
insert into ${schema.sessionWorkflowWakeOutbox} (
|
|
1993
|
-
session_id, account_id, workspace_id, temporal_workflow_id, reason
|
|
2163
|
+
session_id, account_id, workspace_id, temporal_workflow_id, reason
|
|
1994
2164
|
)
|
|
1995
|
-
select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}
|
|
2165
|
+
select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}
|
|
1996
2166
|
from eligible
|
|
1997
2167
|
on conflict (session_id) do update set
|
|
1998
2168
|
wake_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1999
|
-
control_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
2000
2169
|
temporal_workflow_id = excluded.temporal_workflow_id,
|
|
2001
2170
|
reason = excluded.reason,
|
|
2002
2171
|
attempts = 0,
|
|
@@ -2,14 +2,10 @@ import { and, eq } from "drizzle-orm";
|
|
|
2
2
|
import type { Database } from "./database";
|
|
3
3
|
import * as schema from "./schema";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
export const
|
|
7
|
-
|
|
8
|
-
continuationHoldUntil: null,
|
|
9
|
-
continuationHoldReason: null,
|
|
10
|
-
continuationHoldSetAt: null,
|
|
5
|
+
/** Clear a terminal unchanged-input continuation fence after newer goal truth. */
|
|
6
|
+
export const SESSION_GOAL_SUPPRESSION_CLEARED = {
|
|
7
|
+
continuationSuppressedTurnId: null,
|
|
11
8
|
} as const;
|
|
12
|
-
|
|
13
9
|
/** The pause reason written when a per-goal or deployment continuation ceiling is reached. */
|
|
14
10
|
export const SESSION_GOAL_CAP_PAUSED_REASON = "max_auto_continuations";
|
|
15
11
|
|
|
@@ -103,7 +99,7 @@ export async function autoResumeGoalPausedByCapInTransaction(
|
|
|
103
99
|
version: existing.version + 1,
|
|
104
100
|
continuationWakeRevision: existing.continuationWakeRevision + 1,
|
|
105
101
|
...SESSION_GOAL_CONTINUATION_EPOCH_RESET,
|
|
106
|
-
...
|
|
102
|
+
...SESSION_GOAL_SUPPRESSION_CLEARED,
|
|
107
103
|
updatedAt: input.now,
|
|
108
104
|
})
|
|
109
105
|
.where(eq(schema.sessionGoals.id, existing.id))
|