@voicethere/agent 0.5.4 → 0.5.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7C,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,mFAAmF;IACnF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,uBAAuB,EAgHpD,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAMrE,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,eAAe,CAEnE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,CAMnE;AAED,wBAAgB,yBAAyB,IAAI,uBAAuB,EAAE,CAErE"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7C,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,mFAAmF;IACnF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,uBAAuB,EA4HpD,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAMrE,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,eAAe,CAEnE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,CAMnE;AAED,wBAAgB,yBAAyB,IAAI,uBAAuB,EAAE,CAErE"}
@@ -51,6 +51,17 @@ export const AGENT_TEMPLATES = [
51
51
  "voice-showcase/fun-facts.ts",
52
52
  ],
53
53
  },
54
+ {
55
+ id: "recording-consent",
56
+ entry: "recording-consent/agent.ts",
57
+ kind: "product",
58
+ seedOnCreate: true,
59
+ description: "Recording consent flow — ask consent, pause capture for PII, resume when allowed.",
60
+ sourceFiles: [
61
+ "recording-consent/agent.ts",
62
+ "recording-consent/conversation.ts",
63
+ ],
64
+ },
54
65
  {
55
66
  id: "webhooks",
56
67
  entry: "webhooks.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"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"}
@@ -191,6 +191,7 @@ function parseDataChannelPayload(raw) {
191
191
  }
192
192
  }
193
193
  var peerEnvBySessionId = /* @__PURE__ */ new Map();
194
+ var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
194
195
  var endedSessionIds = /* @__PURE__ */ new Set();
195
196
  var pendingRecordingAcks = /* @__PURE__ */ new Map();
196
197
  function handleRecordingControlAck(message) {
@@ -340,6 +341,7 @@ async function handleParentMessage(message, handlers) {
340
341
  case "session_start":
341
342
  endedSessionIds.delete(message.sessionId);
342
343
  peerEnvBySessionId.set(message.sessionId, message.env);
344
+ recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
343
345
  const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
344
346
  if (sessionStartInitDelayMs > 0) {
345
347
  await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
@@ -384,6 +386,7 @@ async function handleParentMessage(message, handlers) {
384
386
  case "session_end":
385
387
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
386
388
  peerEnvBySessionId.delete(message.sessionId);
389
+ recordingAvailableBySessionId.delete(message.sessionId);
387
390
  await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
388
391
  sessionId: message.sessionId
389
392
  });
@@ -669,6 +672,136 @@ function pickFunFact() {
669
672
  var GEOCODE_URL = "https://geocoding-api.open-meteo.com/v1/search";
670
673
  var FORECAST_URL = "https://api.open-meteo.com/v1/forecast";
671
674
  var FETCH_TIMEOUT_MS = 8e3;
675
+ var DIGIT_WORDS = {
676
+ zero: "0",
677
+ oh: "0",
678
+ o: "0",
679
+ one: "1",
680
+ two: "2",
681
+ three: "3",
682
+ four: "4",
683
+ five: "5",
684
+ six: "6",
685
+ seven: "7",
686
+ eight: "8",
687
+ nine: "9"
688
+ };
689
+ var COUNTRY_ALIASES = {
690
+ thailand: "Thailand",
691
+ us: "United States",
692
+ usa: "United States",
693
+ america: "United States",
694
+ "united states": "United States",
695
+ "united states of america": "United States",
696
+ uk: "United Kingdom",
697
+ britain: "United Kingdom",
698
+ england: "United Kingdom",
699
+ "united kingdom": "United Kingdom",
700
+ "great britain": "United Kingdom",
701
+ germany: "Germany",
702
+ france: "France",
703
+ spain: "Spain",
704
+ italy: "Italy",
705
+ japan: "Japan",
706
+ china: "China",
707
+ india: "India",
708
+ australia: "Australia",
709
+ canada: "Canada",
710
+ brazil: "Brazil",
711
+ mexico: "Mexico",
712
+ netherlands: "Netherlands",
713
+ holland: "Netherlands",
714
+ "the netherlands": "Netherlands",
715
+ belgium: "Belgium",
716
+ switzerland: "Switzerland",
717
+ sweden: "Sweden",
718
+ norway: "Norway",
719
+ denmark: "Denmark",
720
+ finland: "Finland",
721
+ poland: "Poland",
722
+ portugal: "Portugal",
723
+ greece: "Greece",
724
+ turkey: "Turkey",
725
+ egypt: "Egypt",
726
+ "south africa": "South Africa",
727
+ "new zealand": "New Zealand",
728
+ ireland: "Ireland",
729
+ singapore: "Singapore",
730
+ malaysia: "Malaysia",
731
+ indonesia: "Indonesia",
732
+ vietnam: "Vietnam",
733
+ philippines: "Philippines",
734
+ "south korea": "South Korea",
735
+ korea: "South Korea",
736
+ taiwan: "Taiwan",
737
+ "hong kong": "Hong Kong",
738
+ israel: "Israel",
739
+ uae: "United Arab Emirates",
740
+ "united arab emirates": "United Arab Emirates",
741
+ "saudi arabia": "Saudi Arabia",
742
+ pakistan: "Pakistan",
743
+ bangladesh: "Bangladesh",
744
+ nigeria: "Nigeria",
745
+ kenya: "Kenya",
746
+ argentina: "Argentina",
747
+ chile: "Chile",
748
+ colombia: "Colombia",
749
+ peru: "Peru",
750
+ austria: "Austria",
751
+ "czech republic": "Czechia",
752
+ czechia: "Czechia",
753
+ romania: "Romania",
754
+ hungary: "Hungary",
755
+ ukraine: "Ukraine"
756
+ };
757
+ function matchCountryName(text) {
758
+ const key = text.trim().toLowerCase().replace(/[.,!?]+$/g, "").replace(/\s+/g, " ");
759
+ if (!key) return null;
760
+ return COUNTRY_ALIASES[key] ?? null;
761
+ }
762
+ function spokenDigitsToPostal(text) {
763
+ const tokens = text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
764
+ const digits = [];
765
+ for (const token of tokens) {
766
+ if (/^\d$/.test(token)) {
767
+ digits.push(token);
768
+ continue;
769
+ }
770
+ const mapped = DIGIT_WORDS[token];
771
+ if (mapped) {
772
+ digits.push(mapped);
773
+ }
774
+ }
775
+ if (digits.length >= 4 && digits.length <= 6) {
776
+ return digits.join("");
777
+ }
778
+ return null;
779
+ }
780
+ function splitTrailingCountry(text) {
781
+ const words = text.trim().split(/\s+/).filter(Boolean);
782
+ if (words.length < 2) return null;
783
+ for (let n = Math.min(3, words.length - 1); n >= 1; n -= 1) {
784
+ const tail = words.slice(-n).join(" ");
785
+ const country = matchCountryName(tail);
786
+ if (country) {
787
+ return { rest: words.slice(0, -n).join(" "), country };
788
+ }
789
+ }
790
+ return null;
791
+ }
792
+ function cityFromRemainder(rest) {
793
+ const trimmed = rest.trim().replace(/[.,!?;:]+$/g, "").replace(/\s+(?:in the|in|of)$/i, "").trim();
794
+ if (!trimmed) return null;
795
+ if (/^\d{4,6}(-\d{4})?$/.test(trimmed)) {
796
+ return trimmed;
797
+ }
798
+ const spoken = spokenDigitsToPostal(trimmed);
799
+ if (spoken) return spoken;
800
+ if (trimmed.length >= 2 && trimmed.length <= 60) {
801
+ return trimmed;
802
+ }
803
+ return null;
804
+ }
672
805
  function wmoCodeToPhrase(code) {
673
806
  if (code === 0) return "clear sky";
674
807
  if (code <= 3) return "partly cloudy";
@@ -689,29 +822,48 @@ function formatWeatherSpeech(result) {
689
822
  function parseLocationUtterance(utterance) {
690
823
  const text = utterance.trim();
691
824
  if (!text) return null;
825
+ const countryOnly = matchCountryName(text);
826
+ if (countryOnly) {
827
+ return { country: countryOnly };
828
+ }
829
+ const trailing = splitTrailingCountry(text);
830
+ if (trailing) {
831
+ const city = cityFromRemainder(trailing.rest);
832
+ if (city) {
833
+ return { city, country: trailing.country };
834
+ }
835
+ return { country: trailing.country };
836
+ }
692
837
  const inMatch = text.match(
693
838
  /^(?:in\s+)?(.+?)\s+in\s+([a-zA-Z][\w\s.-]{1,40})$/i
694
839
  );
695
840
  if (inMatch) {
696
- return { city: inMatch[1].trim(), country: inMatch[2].trim() };
841
+ const country = matchCountryName(inMatch[2]) ?? inMatch[2].trim();
842
+ return { city: inMatch[1].trim(), country };
697
843
  }
698
844
  const commaMatch = text.match(/^(.+?),\s*([a-zA-Z][\w\s.-]{1,40})$/);
699
845
  if (commaMatch) {
700
- return { city: commaMatch[1].trim(), country: commaMatch[2].trim() };
846
+ const country = matchCountryName(commaMatch[2]) ?? commaMatch[2].trim();
847
+ return { city: commaMatch[1].trim(), country };
701
848
  }
702
849
  const countryMatch = text.match(
703
850
  /^(.+?)\s+(?:country\s+)?([a-zA-Z][\w\s.-]{2,40})$/i
704
851
  );
705
852
  if (countryMatch && countryMatch[2].split(/\s+/).length <= 3) {
706
853
  const city = countryMatch[1].trim();
707
- const country = countryMatch[2].trim();
854
+ const countryRaw = countryMatch[2].trim();
855
+ const country = matchCountryName(countryRaw) ?? countryRaw;
708
856
  if (city.length >= 2 && country.length >= 2) {
709
857
  return { city, country };
710
858
  }
711
859
  }
712
- if (/^\d{5}(-\d{4})?$/.test(text)) {
860
+ if (/^\d{4,6}(-\d{4})?$/.test(text)) {
713
861
  return { city: text };
714
862
  }
863
+ const spokenPostal = spokenDigitsToPostal(text);
864
+ if (spokenPostal) {
865
+ return { city: spokenPostal };
866
+ }
715
867
  if (text.length >= 2 && text.length <= 60) {
716
868
  return { city: text };
717
869
  }
@@ -1059,8 +1211,15 @@ function handleUtterance(state, utterance) {
1059
1211
  }
1060
1212
  case "weatherAwaitingLocation": {
1061
1213
  const parsed = parseLocationUtterance(text);
1062
- const city = parsed?.city ?? state.weatherCity;
1063
- const country = parsed?.country ?? state.weatherCountry;
1214
+ let city = parsed?.city || state.weatherCity;
1215
+ let country = parsed?.country || state.weatherCountry;
1216
+ if (state.weatherCity && !country) {
1217
+ const followUp = matchCountryName(text) ?? (parsed?.city ? matchCountryName(parsed.city) : null);
1218
+ if (followUp) {
1219
+ city = state.weatherCity;
1220
+ country = followUp;
1221
+ }
1222
+ }
1064
1223
  if (!city) {
1065
1224
  const ask = speakAndChat(
1066
1225
  "Please tell me a city or ZIP code and the country."
@@ -1071,7 +1230,7 @@ function handleUtterance(state, utterance) {
1071
1230
  messages: ask.messages
1072
1231
  };
1073
1232
  }
1074
- if (!country && !parsed?.country && !state.weatherCountry) {
1233
+ if (!country) {
1075
1234
  return {
1076
1235
  state: {
1077
1236
  ...state,
@@ -1084,12 +1243,11 @@ function handleUtterance(state, utterance) {
1084
1243
  ]
1085
1244
  };
1086
1245
  }
1087
- const resolvedCountry = country ?? state.weatherCountry;
1088
1246
  return {
1089
- state: { ...state, weatherCity: city, weatherCountry: resolvedCountry },
1247
+ state: { ...state, weatherCity: city, weatherCountry: country },
1090
1248
  speakLines: [],
1091
1249
  messages: [],
1092
- pendingWeather: { city, country: resolvedCountry }
1250
+ pendingWeather: { city, country }
1093
1251
  };
1094
1252
  }
1095
1253
  case "countAwaitingNumber": {
@@ -191,6 +191,7 @@ function parseDataChannelPayload(raw) {
191
191
  }
192
192
  }
193
193
  var peerEnvBySessionId = /* @__PURE__ */ new Map();
194
+ var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
194
195
  var endedSessionIds = /* @__PURE__ */ new Set();
195
196
  var pendingRecordingAcks = /* @__PURE__ */ new Map();
196
197
  function handleRecordingControlAck(message) {
@@ -340,6 +341,7 @@ async function handleParentMessage(message, handlers) {
340
341
  case "session_start":
341
342
  endedSessionIds.delete(message.sessionId);
342
343
  peerEnvBySessionId.set(message.sessionId, message.env);
344
+ recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
343
345
  const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
344
346
  if (sessionStartInitDelayMs > 0) {
345
347
  await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
@@ -384,6 +386,7 @@ async function handleParentMessage(message, handlers) {
384
386
  case "session_end":
385
387
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
386
388
  peerEnvBySessionId.delete(message.sessionId);
389
+ recordingAvailableBySessionId.delete(message.sessionId);
387
390
  await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
388
391
  sessionId: message.sessionId
389
392
  });
@@ -194,6 +194,7 @@ function parseDataChannelPayload(raw) {
194
194
  }
195
195
  }
196
196
  var peerEnvBySessionId = /* @__PURE__ */ new Map();
197
+ var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
197
198
  var endedSessionIds = /* @__PURE__ */ new Set();
198
199
  var pendingRecordingAcks = /* @__PURE__ */ new Map();
199
200
  function handleRecordingControlAck(message) {
@@ -343,6 +344,7 @@ async function handleParentMessage(message, handlers) {
343
344
  case "session_start":
344
345
  endedSessionIds.delete(message.sessionId);
345
346
  peerEnvBySessionId.set(message.sessionId, message.env);
347
+ recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
346
348
  const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
347
349
  if (sessionStartInitDelayMs > 0) {
348
350
  await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
@@ -387,6 +389,7 @@ async function handleParentMessage(message, handlers) {
387
389
  case "session_end":
388
390
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
389
391
  peerEnvBySessionId.delete(message.sessionId);
392
+ recordingAvailableBySessionId.delete(message.sessionId);
390
393
  await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
391
394
  sessionId: message.sessionId
392
395
  });
@@ -10946,6 +10946,7 @@ function parseDataChannelPayload(raw) {
10946
10946
  }
10947
10947
  }
10948
10948
  var peerEnvBySessionId = /* @__PURE__ */ new Map();
10949
+ var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
10949
10950
  var endedSessionIds = /* @__PURE__ */ new Set();
10950
10951
  var pendingRecordingAcks = /* @__PURE__ */ new Map();
10951
10952
  function handleRecordingControlAck(message) {
@@ -11095,6 +11096,7 @@ async function handleParentMessage(message, handlers) {
11095
11096
  case "session_start":
11096
11097
  endedSessionIds.delete(message.sessionId);
11097
11098
  peerEnvBySessionId.set(message.sessionId, message.env);
11099
+ recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
11098
11100
  const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
11099
11101
  if (sessionStartInitDelayMs > 0) {
11100
11102
  await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
@@ -11139,6 +11141,7 @@ async function handleParentMessage(message, handlers) {
11139
11141
  case "session_end":
11140
11142
  clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
11141
11143
  peerEnvBySessionId.delete(message.sessionId);
11144
+ recordingAvailableBySessionId.delete(message.sessionId);
11142
11145
  await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
11143
11146
  sessionId: message.sessionId
11144
11147
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voicethere/agent",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "VoiceThere customer agent SDK — IPC types and runtime helpers for sandboxed child bundles",
5
5
  "type": "module",
6
6
  "exports": {
@@ -15,10 +15,10 @@ import {
15
15
 
16
16
  ## Product vs e2e
17
17
 
18
- | Kind | Dashboard create | Prebuilt seed bundle | Typical consumer |
19
- | ----------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------------------- |
20
- | **product** | Yes (`echo`, `echo-dc`, `voice-starter`, `game-sync`, `voice-showcase`, `webhooks`, `webhooks-redis`) | Yes — `dist/templates/<id>/agent.js` | Platform project create |
21
- | **e2e** | No | No — build from sources at test time | `voicethere/e2e` smokes |
18
+ | Kind | Dashboard create | Prebuilt seed bundle | Typical consumer |
19
+ | ----------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------------------- |
20
+ | **product** | Yes (`echo`, `echo-dc`, `voice-starter`, `game-sync`, `voice-showcase`, `recording-consent`, `webhooks`, `webhooks-redis`) | Yes — `dist/templates/<id>/agent.js` | Platform project create |
21
+ | **e2e** | No | No — build from sources at test time | `voicethere/e2e` smokes |
22
22
 
23
23
  Product templates always set `seedOnCreate: true`. CI fails if a product template is missing its prebuilt bundle after `npm run build`.
24
24
 
@@ -67,6 +67,12 @@ Conversational voice demo for landing and dashboard previews — greets the user
67
67
 
68
68
  Sources: `voice-showcase/agent.ts` (defineAgent wiring), `conversation.ts` (pure state machine), `weather.ts`, `recipes.ts`, `fun-facts.ts`.
69
69
 
70
+ ### `recording-consent/` (`recording-consent`)
71
+
72
+ Demonstrates conversation recording consent on connect: asks whether recording is OK when the project has recording enabled, pauses capture while collecting name and date of birth, then resumes only if the customer consented. When project recording is off, logs a warning and never calls `startRecording`. Voice finals and typed chat share the same handler.
73
+
74
+ Sources: `recording-consent/agent.ts` (defineAgent wiring), `conversation.ts` (pure state machine).
75
+
70
76
  ### `webhooks.ts` (`webhooks`)
71
77
 
72
78
  Inbound HTTP webhook sample — verifies `x-agent-webhook-signature` HMAC on the **raw body** with `AGENT_WEBHOOK_SIGNING_SECRET`, then `JSON.parse` and fans out to connected sessions via DataChannel + `speak`.
@@ -19,6 +19,19 @@ export interface UnregisterCommand {
19
19
  objectId?: number;
20
20
  }
21
21
 
22
+ export function resolveRemoveTarget(
23
+ objectId: number | undefined,
24
+ ownedObjectIds: ReadonlySet<number> | undefined | null,
25
+ ): { ok: true; objectId: number } | { ok: false; reason: string } {
26
+ if (objectId !== undefined) {
27
+ return { ok: true, objectId };
28
+ }
29
+ if (!ownedObjectIds || ownedObjectIds.size === 0) {
30
+ return { ok: false, reason: UNREGISTER_NACK_REASON_NOT_FOUND };
31
+ }
32
+ return { ok: true, objectId: Math.max(...ownedObjectIds) };
33
+ }
34
+
22
35
  export function parseUnregisterCommand(
23
36
  message: unknown,
24
37
  ): UnregisterCommand | null {
@@ -124,6 +124,33 @@ export function writeObjectSlot(
124
124
  world[start + 8] = dirW;
125
125
  }
126
126
 
127
+ /**
128
+ * After simulating from a Redis load, keep slots empty that a concurrent release
129
+ * zeroed in Redis while the tick was in flight — prevents saveWorldToRedis from
130
+ * restoring an object Lua just cleared (sim GET → simulate → SET race).
131
+ *
132
+ * `authoritative` must be a fresh Redis GET after simulate (never stale in-memory
133
+ * worldState — an empty buffer would wipe every live slot).
134
+ */
135
+ export function preserveEmptySlots(
136
+ simulated: Float32Array,
137
+ authoritative: Float32Array,
138
+ ): void {
139
+ for (let slot = 0; slot < MAX_LIVE_OBJECTS; slot += 1) {
140
+ if (readSlotObjectId(authoritative, slot) === 0) {
141
+ markSlotFree(simulated, slot);
142
+ }
143
+ }
144
+ }
145
+
146
+ /** Merge a simulated world with the latest Redis snapshot before SET. */
147
+ export function commitSimulatedWorld(
148
+ simulated: Float32Array,
149
+ latestRedis: Float32Array,
150
+ ): void {
151
+ preserveEmptySlots(simulated, latestRedis);
152
+ }
153
+
127
154
  export function collectActiveObjectIds(world: Float32Array): number[] {
128
155
  const ids: number[] = [];
129
156
  for (let slot = 0; slot < MAX_LIVE_OBJECTS; slot += 1) {