@opengeni/db 1.4.0 → 1.4.2

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.
Files changed (68) hide show
  1. package/dist/canonical-human-identities.js +2 -2
  2. package/dist/capability-reconciliation.js +1 -1
  3. package/dist/{chunk-7QWWTVYH.js → chunk-5ROHLEN6.js} +29 -4
  4. package/dist/chunk-5ROHLEN6.js.map +1 -0
  5. package/dist/{chunk-BA52ITIY.js → chunk-A4J7EECR.js} +2 -2
  6. package/dist/{chunk-3WUCM7YF.js → chunk-B5C5SBSI.js} +2 -2
  7. package/dist/{chunk-VCHK4D6S.js → chunk-DKFEUGM4.js} +2 -2
  8. package/dist/{chunk-EFANLEWI.js → chunk-HUKBIZGH.js} +2 -2
  9. package/dist/{chunk-27T4AP25.js → chunk-IT3EYY2C.js} +5 -3
  10. package/dist/{chunk-27T4AP25.js.map → chunk-IT3EYY2C.js.map} +1 -1
  11. package/dist/{chunk-NT7OGKYX.js → chunk-JWBEE4CL.js} +3 -3
  12. package/dist/{chunk-AMK3FNOW.js → chunk-K5MTNKJU.js} +3 -3
  13. package/dist/{chunk-GC5GUTV3.js → chunk-NZ2SP55O.js} +2 -2
  14. package/dist/{chunk-TRUEH4I2.js → chunk-OKCBARX6.js} +2 -2
  15. package/dist/{chunk-EMYA6XNE.js → chunk-OV5AUIBA.js} +2 -2
  16. package/dist/{chunk-T3CYF3DH.js → chunk-TODVDGMO.js} +35 -17
  17. package/dist/{chunk-T3CYF3DH.js.map → chunk-TODVDGMO.js.map} +1 -1
  18. package/dist/{chunk-CKDHKQS4.js → chunk-W75DRYG3.js} +4 -4
  19. package/dist/{chunk-UQELJ6OM.js → chunk-Y72DP3JF.js} +2 -2
  20. package/dist/codemode-approvals.d.ts +7 -1
  21. package/dist/connect-attempts.js +2 -2
  22. package/dist/credit-balances.d.ts +25 -4
  23. package/dist/database.d.ts +7 -1
  24. package/dist/editable-artifact-durable-export.js +1 -1
  25. package/dist/editable-artifacts.js +2 -2
  26. package/dist/index.d.ts +24 -2
  27. package/dist/index.js +2778 -2508
  28. package/dist/index.js.map +1 -1
  29. package/dist/managed-auth-session-sets.js +2 -2
  30. package/dist/mcp-operations.js +1 -1
  31. package/dist/modal-native-live-origin.d.ts +75 -0
  32. package/dist/organization-integration-policy.js +2 -2
  33. package/dist/provision-roles.js +1 -1
  34. package/dist/retained-provider-commands.d.ts +21 -21
  35. package/dist/retained-provider-commands.js +5 -5
  36. package/dist/runtime-posture.d.ts +1 -1
  37. package/dist/sandbox-interaction-idle.d.ts +22 -0
  38. package/dist/session-background-commands.js +3 -3
  39. package/dist/session-command-output.js +4 -4
  40. package/dist/session-event-slices.js +2 -2
  41. package/dist/session-mcp-credential-rotation.js +4 -4
  42. package/dist/session-tenancy.js +2 -2
  43. package/dist/video-generation.js +2 -2
  44. package/dist/workspace-tool-defaults.js +3 -3
  45. package/drizzle/0632_modal_native_live_origin.sql +239 -0
  46. package/package.json +7 -7
  47. package/src/codemode-approvals.ts +52 -22
  48. package/src/codemode-operations.ts +198 -181
  49. package/src/credit-balances.ts +58 -17
  50. package/src/database.ts +9 -0
  51. package/src/index.ts +449 -254
  52. package/src/modal-native-live-origin.ts +148 -0
  53. package/src/provision-roles.ts +12 -0
  54. package/src/runtime-posture.ts +32 -3
  55. package/src/sandbox-interaction-idle.ts +91 -0
  56. package/src/transcription-recordings.ts +3 -1
  57. package/dist/chunk-7QWWTVYH.js.map +0 -1
  58. /package/dist/{chunk-BA52ITIY.js.map → chunk-A4J7EECR.js.map} +0 -0
  59. /package/dist/{chunk-3WUCM7YF.js.map → chunk-B5C5SBSI.js.map} +0 -0
  60. /package/dist/{chunk-VCHK4D6S.js.map → chunk-DKFEUGM4.js.map} +0 -0
  61. /package/dist/{chunk-EFANLEWI.js.map → chunk-HUKBIZGH.js.map} +0 -0
  62. /package/dist/{chunk-NT7OGKYX.js.map → chunk-JWBEE4CL.js.map} +0 -0
  63. /package/dist/{chunk-AMK3FNOW.js.map → chunk-K5MTNKJU.js.map} +0 -0
  64. /package/dist/{chunk-GC5GUTV3.js.map → chunk-NZ2SP55O.js.map} +0 -0
  65. /package/dist/{chunk-TRUEH4I2.js.map → chunk-OKCBARX6.js.map} +0 -0
  66. /package/dist/{chunk-EMYA6XNE.js.map → chunk-OV5AUIBA.js.map} +0 -0
  67. /package/dist/{chunk-CKDHKQS4.js.map → chunk-W75DRYG3.js.map} +0 -0
  68. /package/dist/{chunk-UQELJ6OM.js.map → chunk-Y72DP3JF.js.map} +0 -0
@@ -0,0 +1,148 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { rawRows, type Database } from "./database";
3
+
4
+ /** Structural V2 correlation scope. This is deliberately NOT an actor/grant. */
5
+ export type ModalNativeLiveOriginScope = Readonly<{
6
+ version: 2;
7
+ declarationId: string;
8
+ planId: string;
9
+ creatorId: string;
10
+ accountId: string;
11
+ workspaceId: string;
12
+ sessionId: string;
13
+ turnId: string;
14
+ attemptId: string;
15
+ executionGeneration: number;
16
+ triggerEventId: string;
17
+ sandboxGroupId: string;
18
+ routeKind: "home";
19
+ routeTargetId: null;
20
+ routeEpoch: number;
21
+ }>;
22
+
23
+ export type ModalNativeLiveOriginProjection = Readonly<{
24
+ version: 1;
25
+ scope: ModalNativeLiveOriginScope;
26
+ initiator: {
27
+ kind: "subject" | "service";
28
+ subjectId: string;
29
+ initiatingHumanSubjectId: string;
30
+ };
31
+ membership: {
32
+ id: string;
33
+ authorizationRevision: string;
34
+ basis: "workspace_membership" | "personal_workspace";
35
+ };
36
+ acceptedAuthority: {
37
+ epoch: number;
38
+ visibility: "user_private" | "workspace_shared";
39
+ ownerOrganizationMembershipId: string | null;
40
+ };
41
+ currentAuthority: {
42
+ epoch: number;
43
+ executionEpoch: number;
44
+ visibility: "user_private" | "workspace_shared";
45
+ ownerSubjectId: string | null;
46
+ ownerOrganizationMembershipId: string | null;
47
+ };
48
+ control: { workspaceRevision: string; sessionVersion: string };
49
+ execution: { workflowId: string; workflowRunId: string; activityId: string };
50
+ providerRecoveryCount: 0 | 1 | 2 | 3 | 4 | 5;
51
+ checkedAt: string;
52
+ }>;
53
+
54
+ export type ModalNativeLiveOriginResult =
55
+ | { kind: "live"; projection: ModalNativeLiveOriginProjection }
56
+ | { kind: "fenced" }
57
+ | {
58
+ kind: "unsupported";
59
+ reason: "invalid_scope" | "human_unavailable" | "external_authority" | "not_premodel";
60
+ };
61
+
62
+ const keys = [
63
+ "version",
64
+ "declarationId",
65
+ "planId",
66
+ "creatorId",
67
+ "accountId",
68
+ "workspaceId",
69
+ "sessionId",
70
+ "turnId",
71
+ "attemptId",
72
+ "executionGeneration",
73
+ "triggerEventId",
74
+ "sandboxGroupId",
75
+ "routeKind",
76
+ "routeTargetId",
77
+ "routeEpoch",
78
+ ] as const;
79
+ const ids = [
80
+ "declarationId",
81
+ "planId",
82
+ "creatorId",
83
+ "accountId",
84
+ "workspaceId",
85
+ "sessionId",
86
+ "turnId",
87
+ "attemptId",
88
+ "triggerEventId",
89
+ "sandboxGroupId",
90
+ ] as const;
91
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
92
+
93
+ function scopeSnapshot(value: ModalNativeLiveOriginScope): ModalNativeLiveOriginScope | null {
94
+ if (!value || typeof value !== "object") return null;
95
+ const descriptors = Object.getOwnPropertyDescriptors(value);
96
+ if (Reflect.ownKeys(value).length !== keys.length) return null;
97
+ const copy: Record<string, unknown> = {};
98
+ for (const key of keys) {
99
+ const descriptor = descriptors[key];
100
+ if (!descriptor || !("value" in descriptor)) return null;
101
+ copy[key] = descriptor.value;
102
+ }
103
+ if (ids.some((key) => typeof copy[key] !== "string" || !uuid.test(copy[key] as string)))
104
+ return null;
105
+ if (
106
+ copy.version !== 2 ||
107
+ copy.routeKind !== "home" ||
108
+ copy.routeTargetId !== null ||
109
+ copy.creatorId === copy.attemptId ||
110
+ copy.creatorId === copy.planId ||
111
+ !Number.isSafeInteger(copy.executionGeneration) ||
112
+ (copy.executionGeneration as number) < 1 ||
113
+ (copy.executionGeneration as number) > 2_147_483_647 ||
114
+ !Number.isSafeInteger(copy.routeEpoch) ||
115
+ (copy.routeEpoch as number) < 0 ||
116
+ (copy.routeEpoch as number) > 2_147_483_647
117
+ )
118
+ return null;
119
+ return Object.freeze(copy) as ModalNativeLiveOriginScope;
120
+ }
121
+
122
+ /**
123
+ * Private-server integration primitive, NOT host authentication or a grant.
124
+ * The caller must authenticate the exact host request separately and consume
125
+ * this projection in this SAME transaction. No native/provider awaits here.
126
+ * Enter before taking any tenancy/control/session/lease lock: SQL acquires the
127
+ * organization-membership -> tenancy -> canonical control/row prefix itself.
128
+ * Preserves all subject and tenant GUCs. SQL uncertainty propagates (including
129
+ * 55P03); no failed/aborted transaction is reported as successful admission.
130
+ */
131
+ export async function lockLiveNativeOriginalOriginTx(
132
+ scopedTx: Database,
133
+ scope: ModalNativeLiveOriginScope,
134
+ ): Promise<ModalNativeLiveOriginResult> {
135
+ if (typeof (scopedTx as Database & { rollback?: unknown }).rollback !== "function") {
136
+ throw new Error("LIVE native origin projection requires an existing transaction");
137
+ }
138
+ const snapshot = scopeSnapshot(scope);
139
+ if (!snapshot) return { kind: "unsupported", reason: "invalid_scope" };
140
+ const [row] = await rawRows<{ result: ModalNativeLiveOriginResult }>(
141
+ scopedTx,
142
+ sql`select lock_live_native_original_origin_v2(${JSON.stringify(snapshot)}::jsonb) as result`,
143
+ );
144
+ if (!row || !["live", "fenced", "unsupported"].includes(row.result?.kind)) {
145
+ throw new Error("Invalid LIVE native origin database projection");
146
+ }
147
+ return row.result;
148
+ }
@@ -578,6 +578,8 @@ async function grantAppRoleIfSchemaExists(
578
578
  "prepare_organization_membership_protocol_settlements(jsonb)",
579
579
  "assert_active_managed_human_organization_membership(uuid,text)",
580
580
  "resolve_workspace_writer_grant_identity(uuid,text)",
581
+ "lock_live_native_original_origin_v2(jsonb)",
582
+ "modal_native_origin_member_read_active(uuid,text)",
581
583
  "prepare_workspace_membership_removal_settlements(jsonb)",
582
584
  "workspace_membership_removal_command(jsonb)",
583
585
  "get_organization_retention_policy(uuid,text)",
@@ -2105,6 +2107,16 @@ BEGIN
2105
2107
  REVOKE ALL ON FUNCTION opengeni_private.list_pending_modal_provider_creates() FROM PUBLIC;
2106
2108
  END IF;
2107
2109
  END IF;
2110
+ IF to_regclass('opengeni_private.modal_native_origin_read_capabilities') IS NOT NULL THEN
2111
+ EXECUTE format('REVOKE ALL ON TABLE opengeni_private.modal_native_origin_read_capabilities FROM %I', ${literal(role)});
2112
+ EXECUTE format('REVOKE ALL (%s) ON TABLE opengeni_private.modal_native_origin_read_capabilities FROM %I',
2113
+ (SELECT string_agg(quote_ident(attname), ',') FROM pg_attribute
2114
+ WHERE attrelid='opengeni_private.modal_native_origin_read_capabilities'::regclass AND attnum>0 AND NOT attisdropped), ${literal(role)});
2115
+ REVOKE ALL ON TABLE opengeni_private.modal_native_origin_read_capabilities FROM PUBLIC;
2116
+ EXECUTE format('REVOKE ALL (%s) ON TABLE opengeni_private.modal_native_origin_read_capabilities FROM PUBLIC',
2117
+ (SELECT string_agg(quote_ident(attname), ',') FROM pg_attribute
2118
+ WHERE attrelid='opengeni_private.modal_native_origin_read_capabilities'::regclass AND attnum>0 AND NOT attisdropped));
2119
+ END IF;
2108
2120
  IF to_regclass('opengeni_private.sandbox_recovery_rollout') IS NOT NULL THEN
2109
2121
  -- Migration may precede this role's creation. Converge only read access;
2110
2122
  -- runtime identities and PUBLIC never receive recovery activation writes.
@@ -609,6 +609,8 @@ const PRIVATE_SESSION_CREATE_CAPABILITY_ROUTINES = [
609
609
  "close_private_session_create_capability(uuid)",
610
610
  ] as const;
611
611
  const SESSION_AUTHORITY_ROUTINES = new Set<string>([
612
+ "lock_live_native_original_origin_v2(jsonb)",
613
+ "modal_native_origin_member_read_active(uuid, text)",
612
614
  LEGACY_FORK_SESSION_CONTENT_ROUTINE,
613
615
  FORK_SESSION_CONTENT_ROUTINE,
614
616
  MESSAGE_FORK_SESSION_CONTENT_ROUTINE,
@@ -712,6 +714,8 @@ const UNIFIED_KNOWLEDGE_AUTHORITY_TABLES = [
712
714
  "documents",
713
715
  ] as const;
714
716
  export const RUNTIME_TARGET_SCHEMA_CAPABILITY_ROUTINES = [
717
+ "lock_live_native_original_origin_v2(jsonb)",
718
+ "modal_native_origin_member_read_active(uuid, text)",
715
719
  ...CLAUDE_SUBSCRIPTION_CAPABILITY_ROUTINES,
716
720
  "maintain_usage_allowances(integer, integer)",
717
721
  "usage_allowance_command(jsonb)",
@@ -2135,15 +2139,15 @@ export async function inspectRuntimeDatabasePosture(
2135
2139
  -- Column-only grants on capability tables are also unsafe; a pin
2136
2140
  -- table must remain EXECUTE-only even after column ACL drift.
2137
2141
  (has_table_privilege(current_user, c.oid, 'SELECT') or
2138
- ((c.relname in ('modal_inventory_read_capabilities','usage_allowance_capabilities','session_import_batches', ${ARTIFACT_PINS_TABLE}) or
2142
+ ((c.relname in ('modal_native_origin_read_capabilities','modal_inventory_read_capabilities','usage_allowance_capabilities','session_import_batches', ${ARTIFACT_PINS_TABLE}) or
2139
2143
  c.relname in (select jsonb_array_elements_text(${JSON.stringify(RUNTIME_ALLOWANCE_PRIVATE_TABLES)}::jsonb))) and
2140
2144
  has_any_column_privilege(current_user, c.oid, 'SELECT'))) as can_select,
2141
2145
  (has_table_privilege(current_user, c.oid, 'INSERT') or
2142
- ((c.relname in ('modal_inventory_read_capabilities','usage_allowance_capabilities','session_import_batches', ${ARTIFACT_PINS_TABLE}) or
2146
+ ((c.relname in ('modal_native_origin_read_capabilities','modal_inventory_read_capabilities','usage_allowance_capabilities','session_import_batches', ${ARTIFACT_PINS_TABLE}) or
2143
2147
  c.relname in (select jsonb_array_elements_text(${JSON.stringify(RUNTIME_ALLOWANCE_PRIVATE_TABLES)}::jsonb))) and
2144
2148
  has_any_column_privilege(current_user, c.oid, 'INSERT'))) as can_insert,
2145
2149
  (has_table_privilege(current_user, c.oid, 'UPDATE') or
2146
- ((c.relname in ('modal_inventory_read_capabilities','usage_allowance_capabilities','session_import_batches', ${ARTIFACT_PINS_TABLE}) or
2150
+ ((c.relname in ('modal_native_origin_read_capabilities','modal_inventory_read_capabilities','usage_allowance_capabilities','session_import_batches', ${ARTIFACT_PINS_TABLE}) or
2147
2151
  c.relname in (select jsonb_array_elements_text(${JSON.stringify(RUNTIME_ALLOWANCE_PRIVATE_TABLES)}::jsonb))) and
2148
2152
  has_any_column_privilege(current_user, c.oid, 'UPDATE'))) as can_update,
2149
2153
  has_table_privilege(current_user, c.oid, 'DELETE') as can_delete,
@@ -2190,6 +2194,7 @@ export async function inspectRuntimeDatabasePosture(
2190
2194
  'session_file_read_capabilities',
2191
2195
  'session_import_batches',
2192
2196
  'modal_inventory_read_capabilities',
2197
+ 'modal_native_origin_read_capabilities',
2193
2198
  ${AUTOMATIC_SESSION_TITLE_FANOUT_OUTBOX_TABLE},
2194
2199
  ${VERIFIED_SIGNUP_TRIAL_SWITCH_TABLE},
2195
2200
  'credit_promotion_policy_revisions',
@@ -4235,6 +4240,30 @@ export function evaluateRuntimeDatabasePosture(
4235
4240
  ) {
4236
4241
  violations.push("usage allowance attribution receipts lack same-owner FORCE-RLS isolation");
4237
4242
  }
4243
+ const nativeOriginCapability = posture.privateTables.find(
4244
+ (table) => table.name === "modal_native_origin_read_capabilities",
4245
+ );
4246
+ const nativeOriginRoutineInstalled = posture.targetRoutines.some(
4247
+ (routine) => routine.name === "lock_live_native_original_origin_v2(jsonb)",
4248
+ );
4249
+ if (
4250
+ (nativeOriginRoutineInstalled || nativeOriginCapability) &&
4251
+ (!nativeOriginCapability ||
4252
+ nativeOriginCapability.owner === expectedRole ||
4253
+ nativeOriginCapability.owner !== tableByName.get("sessions")?.owner ||
4254
+ nativeOriginCapability.owner !== tableByName.get("organization_memberships")?.owner ||
4255
+ !nativeOriginCapability.rlsEnabled ||
4256
+ !nativeOriginCapability.rlsForced ||
4257
+ !nativeOriginCapability.rlsActive ||
4258
+ nativeOriginCapability.select ||
4259
+ nativeOriginCapability.insert ||
4260
+ nativeOriginCapability.update ||
4261
+ nativeOriginCapability.delete ||
4262
+ nativeOriginCapability.truncate ||
4263
+ nativeOriginCapability.references ||
4264
+ nativeOriginCapability.trigger)
4265
+ )
4266
+ violations.push("Native LIVE-origin read capability has unsafe owner, RLS or privileges");
4238
4267
  const modalInventoryCapability = posture.privateTables.find(
4239
4268
  (table) => table.name === "modal_inventory_read_capabilities",
4240
4269
  );
@@ -0,0 +1,91 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { rawRows, withRlsContext, type Database } from "./database";
3
+
4
+ export const INTERACTION_ONLY_IDLE_BUCKETS = ["lt_30m", "30m_2h", "2h_6h", "gte_6h"] as const;
5
+ export type InteractionOnlyIdleBucket = (typeof INTERACTION_ONLY_IDLE_BUCKETS)[number];
6
+
7
+ export type InteractionOnlyLeaseCandidate = {
8
+ accountId: string;
9
+ workspaceId: string;
10
+ sandboxGroupId: string;
11
+ };
12
+
13
+ export function interactionOnlyIdleBucket(idleMs: number): InteractionOnlyIdleBucket {
14
+ if (idleMs < 30 * 60_000) return "lt_30m";
15
+ if (idleMs < 2 * 60 * 60_000) return "30m_2h";
16
+ if (idleMs < 6 * 60 * 60_000) return "2h_6h";
17
+ return "gte_6h";
18
+ }
19
+
20
+ /**
21
+ * Read-only inventory of warm leases whose ONLY holders are Browser/Computer
22
+ * interaction holders, bucketed by time since the newest interaction activity
23
+ * (holder heartbeat, controller heartbeat, or last use). Interaction holders
24
+ * deliberately never expire by timestamp, so such a box stays warm until its
25
+ * session ends or the provider deadline; this makes that visible. Candidates
26
+ * come from the sanctioned cross-workspace warm-lease list; each workspace is
27
+ * then read under its own RLS scope. No lock beyond the snapshot is taken.
28
+ */
29
+ export async function countInteractionOnlyWarmLeasesByIdle(
30
+ db: Database,
31
+ candidates: InteractionOnlyLeaseCandidate[],
32
+ options: { now?: Date; maxWorkspaces?: number } = {},
33
+ ): Promise<Record<InteractionOnlyIdleBucket, number>> {
34
+ const nowMs = (options.now ?? new Date()).getTime();
35
+ const counts = Object.fromEntries(INTERACTION_ONLY_IDLE_BUCKETS.map((b) => [b, 0])) as Record<
36
+ InteractionOnlyIdleBucket,
37
+ number
38
+ >;
39
+ const byWorkspace = new Map<string, InteractionOnlyLeaseCandidate[]>();
40
+ for (const candidate of candidates) {
41
+ const key = `${candidate.accountId}:${candidate.workspaceId}`;
42
+ byWorkspace.set(key, [...(byWorkspace.get(key) ?? []), candidate]);
43
+ }
44
+ const workspaces = [...byWorkspace.values()].slice(0, options.maxWorkspaces ?? 500);
45
+ for (const group of workspaces) {
46
+ const { accountId, workspaceId } = group[0]!;
47
+ const groupIds = group.map((candidate) => candidate.sandboxGroupId);
48
+ const rows = await withRlsContext(
49
+ db,
50
+ { accountId, workspaceId },
51
+ async (scoped) =>
52
+ await rawRows<{ last_activity_at: Date | string | null }>(
53
+ scoped,
54
+ sql`
55
+ select max(greatest(
56
+ holder.last_heartbeat_at,
57
+ browser.controller_heartbeat_at, browser.last_used_at,
58
+ computer.controller_heartbeat_at, computer.last_used_at
59
+ )) as last_activity_at
60
+ from sandbox_leases lease
61
+ join sandbox_lease_holders holder
62
+ on holder.lease_id = lease.id and holder.workspace_id = lease.workspace_id
63
+ left join browser_sessions browser
64
+ on holder.holder_id = 'browser-session:' || browser.id::text
65
+ and browser.workspace_id = lease.workspace_id
66
+ left join computer_sessions computer
67
+ on holder.holder_id = 'computer-session:' || computer.id::text
68
+ and computer.workspace_id = lease.workspace_id
69
+ where lease.account_id = ${accountId}
70
+ and lease.workspace_id = ${workspaceId}
71
+ and lease.sandbox_group_id in ${sql`(${sql.join(
72
+ groupIds.map((id) => sql`${id}`),
73
+ sql`, `,
74
+ )})`}
75
+ and lease.liveness = 'warm'
76
+ group by lease.id
77
+ having bool_and(holder.kind = 'interaction')
78
+ `,
79
+ ),
80
+ undefined,
81
+ "none",
82
+ );
83
+ for (const row of rows) {
84
+ const last = row.last_activity_at ? new Date(row.last_activity_at).getTime() : Number.NaN;
85
+ counts[
86
+ interactionOnlyIdleBucket(Number.isFinite(last) ? Math.max(0, nowMs - last) : Infinity)
87
+ ] += 1;
88
+ }
89
+ }
90
+ return counts;
91
+ }
@@ -1087,7 +1087,9 @@ export function assembleTranscriptionSegments(
1087
1087
  ).trim(),
1088
1088
  )
1089
1089
  .filter(Boolean)
1090
- .join("\n\n");
1090
+ // Segments are fixed-length time cuts, not paragraphs: a boundary usually
1091
+ // falls mid-sentence, so a long dictation must read as one continuous text.
1092
+ .join(" ");
1091
1093
  const seen = new Set<string>();
1092
1094
  const combinedLanguages: string[] = [];
1093
1095
  for (const segment of ordered) {