@voicethere/agent 0.5.5 → 0.6.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.
@@ -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,EA6HpD,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"}
@@ -46,11 +46,23 @@ export const AGENT_TEMPLATES = [
46
46
  sourceFiles: [
47
47
  "voice-showcase/agent.ts",
48
48
  "voice-showcase/conversation.ts",
49
+ "voice-showcase/delivery.ts",
49
50
  "voice-showcase/weather.ts",
50
51
  "voice-showcase/recipes.ts",
51
52
  "voice-showcase/fun-facts.ts",
52
53
  ],
53
54
  },
55
+ {
56
+ id: "recording-consent",
57
+ entry: "recording-consent/agent.ts",
58
+ kind: "product",
59
+ seedOnCreate: true,
60
+ description: "Recording consent flow — ask consent, pause capture for PII, resume when allowed.",
61
+ sourceFiles: [
62
+ "recording-consent/agent.ts",
63
+ "recording-consent/conversation.ts",
64
+ ],
65
+ },
54
66
  {
55
67
  id: "webhooks",
56
68
  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
  });
@@ -1314,6 +1317,36 @@ async function resolveWeatherTurn(state, city, country, fetchFn) {
1314
1317
  }
1315
1318
  }
1316
1319
 
1320
+ // templates/voice-showcase/delivery.ts
1321
+ function spokenThenPlayOps(messages, speakLines) {
1322
+ const ops = [];
1323
+ for (const message of messages) {
1324
+ ops.push({ kind: "send", message });
1325
+ }
1326
+ for (const text of speakLines) {
1327
+ ops.push({ kind: "play", text });
1328
+ }
1329
+ return ops;
1330
+ }
1331
+ function greetingOps(sessionId, greeting) {
1332
+ return spokenThenPlayOps(
1333
+ [
1334
+ { type: "agent_event", event: "session_start", sessionId },
1335
+ { type: "chat_reply", text: greeting }
1336
+ ],
1337
+ [greeting]
1338
+ );
1339
+ }
1340
+ function applyOutboundOps(sessionId, ops, deps) {
1341
+ for (const op of ops) {
1342
+ if (op.kind === "send") {
1343
+ deps.sendToClient(sessionId, op.message);
1344
+ } else {
1345
+ deps.speak(sessionId, op.text);
1346
+ }
1347
+ }
1348
+ }
1349
+
1317
1350
  // templates/voice-showcase/agent.ts
1318
1351
  var sessions = /* @__PURE__ */ new Map();
1319
1352
  function getState(sessionId) {
@@ -1332,20 +1365,16 @@ function relaySpeechEvent(sessionId, event) {
1332
1365
  raw: event
1333
1366
  });
1334
1367
  }
1335
- function deliverMessages(sessionId, messages) {
1336
- for (const message of messages) {
1337
- sendToClient(sessionId, message);
1338
- }
1339
- }
1340
- function speakLines(sessionId, lines) {
1341
- for (const line of lines) {
1342
- speak(sessionId, line);
1343
- }
1368
+ function deliverSpokenThenPlay(sessionId, result) {
1369
+ applyOutboundOps(
1370
+ sessionId,
1371
+ spokenThenPlayOps(result.messages, result.speakLines),
1372
+ { sendToClient, speak }
1373
+ );
1344
1374
  }
1345
1375
  async function applyTurn(sessionId, result) {
1346
1376
  sessions.set(sessionId, result.state);
1347
- speakLines(sessionId, result.speakLines);
1348
- deliverMessages(sessionId, result.messages);
1377
+ deliverSpokenThenPlay(sessionId, result);
1349
1378
  if (result.pendingWeather) {
1350
1379
  const weatherResult = await resolveWeatherTurn(
1351
1380
  result.state,
@@ -1353,8 +1382,7 @@ async function applyTurn(sessionId, result) {
1353
1382
  result.pendingWeather.country
1354
1383
  );
1355
1384
  sessions.set(sessionId, weatherResult.state);
1356
- speakLines(sessionId, weatherResult.speakLines);
1357
- deliverMessages(sessionId, weatherResult.messages);
1385
+ deliverSpokenThenPlay(sessionId, weatherResult);
1358
1386
  }
1359
1387
  }
1360
1388
  async function onUserText(sessionId, text) {
@@ -1365,13 +1393,10 @@ async function onUserText(sessionId, text) {
1365
1393
  defineAgent({
1366
1394
  onSessionStart({ sessionId }) {
1367
1395
  sessions.set(sessionId, createInitialState());
1368
- sendToClient(sessionId, {
1369
- type: "agent_event",
1370
- event: "session_start",
1371
- sessionId
1396
+ applyOutboundOps(sessionId, greetingOps(sessionId, GREETING), {
1397
+ sendToClient,
1398
+ speak
1372
1399
  });
1373
- speak(sessionId, GREETING);
1374
- sendToClient(sessionId, { type: "chat_reply", text: GREETING });
1375
1400
  agentLog("info", `voice-showcase session_start ${sessionId}`);
1376
1401
  },
1377
1402
  onSpeechEvent({ sessionId }, event) {
@@ -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.5",
3
+ "version": "0.6.0",
4
4
  "description": "VoiceThere customer agent SDK — IPC types and runtime helpers for sandboxed child bundles",
5
5
  "type": "module",
6
6
  "exports": {
@@ -28,6 +28,12 @@
28
28
  "require": "./dist/templates/index.js",
29
29
  "default": "./dist/templates/index.js"
30
30
  },
31
+ "./build": {
32
+ "types": "./dist/build-bundle.d.ts",
33
+ "import": "./dist/build-bundle.js",
34
+ "require": "./dist/build-bundle.js",
35
+ "default": "./dist/build-bundle.js"
36
+ },
31
37
  "./templates/redis-sync/world-layout": {
32
38
  "types": "./templates/redis-sync/world-layout.ts",
33
39
  "import": "./templates/redis-sync/world-layout.ts",
@@ -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
 
@@ -63,9 +63,15 @@ When `AGENT_REDIS_URL` is set (project Redis), the world blob is stored at `game
63
63
 
64
64
  ### `voice-showcase/` (`voice-showcase`)
65
65
 
66
- Conversational voice demo for landing and dashboard previews — greets the user, asks for a name, then offers a menu: weather (Open-Meteo, no API key), count 1–10, short recipes, and rotating fun facts. Typed chat and voice finals share the same handler. Sends structured `menu` payloads plus `chat_reply` for the chat log.
66
+ Conversational voice demo for landing and dashboard previews — greets the user, asks for a name, then offers a menu: weather (Open-Meteo, no API key), count 1–10, short recipes, and rotating fun facts. Typed chat and voice finals share the same handler. Sends structured `menu` payloads plus `chat_reply` for the chat log, then triggers TTS play so the client has the spoken text before audio starts.
67
67
 
68
- Sources: `voice-showcase/agent.ts` (defineAgent wiring), `conversation.ts` (pure state machine), `weather.ts`, `recipes.ts`, `fun-facts.ts`.
68
+ Sources: `voice-showcase/agent.ts` (defineAgent wiring), `conversation.ts` (pure state machine), `delivery.ts` (send-then-play order), `weather.ts`, `recipes.ts`, `fun-facts.ts`.
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).
69
75
 
70
76
  ### `webhooks.ts` (`webhooks`)
71
77
 
@@ -162,6 +162,51 @@ export function collectActiveObjectIds(world: Float32Array): number[] {
162
162
  return ids;
163
163
  }
164
164
 
165
+ export interface LiveWorldObjectInfo {
166
+ objectId: number;
167
+ ownerSessionId: string;
168
+ }
169
+
170
+ /** Snapshot occupancy from live world slots; ownerSessionId from map or empty string. */
171
+ export function liveWorldSnapshot(
172
+ world: Float32Array,
173
+ owners: ReadonlyMap<number, string>,
174
+ ): LiveWorldObjectInfo[] {
175
+ const ids = collectActiveObjectIds(world);
176
+ ids.sort((a, b) => a - b);
177
+ return ids.map((objectId) => ({
178
+ objectId,
179
+ ownerSessionId: owners.get(objectId) ?? "",
180
+ }));
181
+ }
182
+
183
+ export function planRedisSimTick(options: {
184
+ lockAcquired: boolean;
185
+ connectedSessions: ReadonlySet<string>;
186
+ }): "simulate" | "relay" | "noop" {
187
+ if (options.lockAcquired) {
188
+ return "simulate";
189
+ }
190
+ if (options.connectedSessions.size > 0) {
191
+ return "relay";
192
+ }
193
+ return "noop";
194
+ }
195
+
196
+ /** Finite positive elapsed seconds, clamped to [1/fallbackHz, 0.05]. */
197
+ export function clampSimulationDtSec(
198
+ elapsedMs: number,
199
+ fallbackHz: number,
200
+ ): number {
201
+ const minDt = 1 / fallbackHz;
202
+ const maxDt = 0.05;
203
+ if (!Number.isFinite(elapsedMs) || elapsedMs <= 0) {
204
+ return minDt;
205
+ }
206
+ const elapsedSec = elapsedMs / 1000;
207
+ return Math.min(maxDt, Math.max(minDt, elapsedSec));
208
+ }
209
+
165
210
  /**
166
211
  * Decode a Redis / Node Buffer into a fixed-size world Float32Array.
167
212
  */
@@ -61,15 +61,17 @@ import {
61
61
  simulateWorldStep,
62
62
  } from "./game-sync-sim.js";
63
63
  import {
64
+ clampSimulationDtSec,
64
65
  collectActiveObjectIds,
65
- commitSimulatedWorld,
66
66
  countLiveObjects,
67
67
  createEmptyWorldBuffer,
68
68
  findFirstEmptySlot,
69
+ liveWorldSnapshot,
69
70
  markSlotFree,
70
71
  normalizeWorldBuffer,
71
72
  objectIdToSlot,
72
73
  OBJECT_SLOT_BYTE_LENGTH,
74
+ planRedisSimTick,
73
75
  REDIS_SIM_LOCK_KEY,
74
76
  REDIS_WORLD_KEY,
75
77
  slotToObjectId,
@@ -92,6 +94,7 @@ let worldState = createEmptyWorldBuffer();
92
94
  let redis: Redis | null = null;
93
95
  let broadcastTimer: NodeJS.Timeout | null = null;
94
96
  let worldMutationChain: Promise<void> = Promise.resolve();
97
+ let lastTickTimeMs = 0;
95
98
 
96
99
  function withWorldMutation<T>(fn: () => Promise<T>): Promise<T> {
97
100
  const run = worldMutationChain.then(fn);
@@ -140,11 +143,6 @@ async function withRedisSimLock<T>(
140
143
  }
141
144
  }
142
145
 
143
- interface TrackedObjectInfo {
144
- objectId: number;
145
- ownerSessionId: string;
146
- }
147
-
148
146
  function rand(min: number, max: number): number {
149
147
  return min + Math.random() * (max - min);
150
148
  }
@@ -315,13 +313,11 @@ async function unregisterObject(
315
313
  return { ok: true, objectId: target.objectId };
316
314
  }
317
315
 
318
- function trackedObjectsSnapshot(): TrackedObjectInfo[] {
319
- const objects: TrackedObjectInfo[] = [];
320
- for (const [objectId, ownerSessionId] of objectOwners) {
321
- objects.push({ objectId, ownerSessionId });
316
+ function broadcastWorldSnapshot(): void {
317
+ const objects = liveWorldSnapshot(worldState, objectOwners);
318
+ for (const sessionId of connectedSessions) {
319
+ sendToClient(sessionId, { type: "world_snapshot", objects });
322
320
  }
323
- objects.sort((a, b) => a.objectId - b.objectId);
324
- return objects;
325
321
  }
326
322
 
327
323
  function notifyObjectRegistered(
@@ -375,33 +371,47 @@ async function saveWorldToRedis(world: Float32Array): Promise<void> {
375
371
  }
376
372
 
377
373
  async function runSimulationTick(): Promise<void> {
374
+ const now = Date.now();
375
+ const dt = clampSimulationDtSec(
376
+ lastTickTimeMs === 0 ? 0 : now - lastTickTimeMs,
377
+ BROADCAST_HZ,
378
+ );
379
+ lastTickTimeMs = now;
380
+
378
381
  if (!redis) {
379
382
  await withWorldMutation(async () => {
380
- const activeObjectIds = [...objectOwners.keys()];
381
- simulateWorldStep(worldState, 1 / BROADCAST_HZ, activeObjectIds);
383
+ simulateWorldStep(worldState, dt, collectActiveObjectIds(worldState));
382
384
  });
383
385
  broadcastWorldBuffer(worldState);
384
386
  return;
385
387
  }
386
388
 
387
389
  await withWorldMutation(async () => {
388
- await withRedisSimLock(
390
+ const lockResult = await withRedisSimLock(
389
391
  async () => {
390
392
  const world = await loadWorldFromRedis();
391
393
  const activeObjectIds = collectActiveObjectIds(world);
392
- simulateWorldStep(world, 1 / BROADCAST_HZ, activeObjectIds);
393
- const latestRedis = await loadWorldFromRedis();
394
- commitSimulatedWorld(world, latestRedis);
394
+ simulateWorldStep(world, dt, activeObjectIds);
395
395
  await saveWorldToRedis(world);
396
396
  worldState = world;
397
+ broadcastWorldBuffer(world);
398
+ return true;
397
399
  },
398
400
  { retryUntilAcquired: false },
399
401
  );
400
- });
401
402
 
402
- const world = await loadWorldFromRedis();
403
- worldState = world;
404
- broadcastWorldBuffer(world);
403
+ if (lockResult === null) {
404
+ const plan = planRedisSimTick({
405
+ lockAcquired: false,
406
+ connectedSessions,
407
+ });
408
+ if (plan === "relay") {
409
+ const world = await loadWorldFromRedis();
410
+ worldState = world;
411
+ broadcastWorldBuffer(world);
412
+ }
413
+ }
414
+ });
405
415
  }
406
416
 
407
417
  function startBroadcastLoopIfNeeded(): void {
@@ -466,12 +476,15 @@ defineAgent({
466
476
  agentLog("info", "game-sync agent connected to project Redis world buffer");
467
477
  },
468
478
 
469
- onClientJoin({ sessionId }) {
479
+ async onClientJoin({ sessionId }) {
470
480
  connectedSessions.add(sessionId);
471
481
  startBroadcastLoopIfNeeded();
482
+ if (redis) {
483
+ worldState = await loadWorldFromRedis();
484
+ }
472
485
  sendToClient(sessionId, {
473
486
  type: "world_snapshot",
474
- objects: trackedObjectsSnapshot(),
487
+ objects: liveWorldSnapshot(worldState, objectOwners),
475
488
  });
476
489
  agentLog("info", `join ${sessionId} connected=${connectedSessions.size}`);
477
490
  },
@@ -516,6 +529,7 @@ defineAgent({
516
529
  }
517
530
  sendToClient(ctx.sessionId, { type: "register_ack", objectId });
518
531
  notifyObjectRegistered(objectId, ctx.sessionId);
532
+ broadcastWorldSnapshot();
519
533
  agentLog(
520
534
  "info",
521
535
  `register session=${ctx.sessionId} objectId=${objectId}`,
@@ -541,6 +555,7 @@ defineAgent({
541
555
  type: "unregister_ack",
542
556
  objectId: result.objectId,
543
557
  });
558
+ broadcastWorldSnapshot();
544
559
  agentLog(
545
560
  "info",
546
561
  `unregister_ack session=${ctx.sessionId} objectId=${result.objectId}`,
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Recording consent template — ask consent, pause for PII, resume when allowed.
3
+ *
4
+ * Build:
5
+ * npx @voicethere/agent build --entry templates/recording-consent/agent.ts
6
+ */
7
+ import {
8
+ agentLog,
9
+ defineAgent,
10
+ parseChatText,
11
+ pauseRecording,
12
+ resumeRecording,
13
+ sendToClient,
14
+ speak,
15
+ stopRecording,
16
+ type SpeechEvent,
17
+ } from "@voicethere/agent";
18
+
19
+ import {
20
+ beginSession,
21
+ handleUtterance,
22
+ type ConversationState,
23
+ type ConversationTurnResult,
24
+ type OutboundMessage,
25
+ type RecordingAction,
26
+ } from "./conversation.js";
27
+
28
+ let sessions = new Map<string, ConversationState>();
29
+ /** Per-session consent flag (shared-child safe — not one process-wide boolean). */
30
+ let consentBySessionId = new Map<string, boolean>();
31
+
32
+ function getState(sessionId: string): ConversationState {
33
+ const state = sessions.get(sessionId);
34
+ if (!state) {
35
+ throw new Error(
36
+ `recording-consent: missing session state for ${sessionId}`,
37
+ );
38
+ }
39
+ return state;
40
+ }
41
+
42
+ function relaySpeechEvent(sessionId: string, event: SpeechEvent): void {
43
+ sendToClient(sessionId, {
44
+ type: "agent_event",
45
+ event: event.type,
46
+ text: event.text,
47
+ raw: event,
48
+ });
49
+ }
50
+
51
+ function deliverMessages(sessionId: string, messages: OutboundMessage[]): void {
52
+ for (const message of messages) {
53
+ sendToClient(sessionId, message);
54
+ }
55
+ }
56
+
57
+ function speakLines(sessionId: string, lines: string[]): void {
58
+ for (const line of lines) {
59
+ speak(sessionId, line);
60
+ }
61
+ }
62
+
63
+ async function applyRecordingAction(
64
+ sessionId: string,
65
+ action: RecordingAction,
66
+ ): Promise<void> {
67
+ if (!action) return;
68
+ switch (action) {
69
+ case "pause":
70
+ await pauseRecording(sessionId);
71
+ break;
72
+ case "stop":
73
+ await stopRecording(sessionId);
74
+ break;
75
+ case "start":
76
+ case "resume":
77
+ await resumeRecording(sessionId);
78
+ break;
79
+ }
80
+ }
81
+
82
+ async function applyTurn(
83
+ sessionId: string,
84
+ result: ConversationTurnResult,
85
+ ): Promise<void> {
86
+ sessions.set(sessionId, result.state);
87
+ if (result.state.consent !== undefined) {
88
+ consentBySessionId.set(sessionId, result.state.consent);
89
+ }
90
+ if (result.warnRecordingDisabled) {
91
+ agentLog(
92
+ "warn",
93
+ "Project conversation recording is disabled; skipping consent and will not call startRecording",
94
+ sessionId,
95
+ );
96
+ }
97
+ speakLines(sessionId, result.speakLines);
98
+ deliverMessages(sessionId, result.messages);
99
+ await applyRecordingAction(sessionId, result.recordingAction);
100
+ }
101
+
102
+ async function onUserText(sessionId: string, text: string): Promise<void> {
103
+ const state = getState(sessionId);
104
+ const result = handleUtterance(state, text);
105
+ await applyTurn(sessionId, result);
106
+ }
107
+
108
+ defineAgent({
109
+ onAgentStart() {
110
+ sessions = new Map();
111
+ consentBySessionId = new Map();
112
+ },
113
+
114
+ onSessionStart({ sessionId, recordingAvailable }) {
115
+ const result = beginSession(recordingAvailable);
116
+ sessions.set(sessionId, result.state);
117
+ if (result.warnRecordingDisabled) {
118
+ agentLog(
119
+ "warn",
120
+ "Project conversation recording is disabled; skipping consent and will not call startRecording",
121
+ sessionId,
122
+ );
123
+ }
124
+ speakLines(sessionId, result.speakLines);
125
+ deliverMessages(sessionId, result.messages);
126
+ sendToClient(sessionId, {
127
+ type: "agent_event",
128
+ event: "session_start",
129
+ sessionId,
130
+ });
131
+ agentLog(
132
+ "info",
133
+ `recording-consent session_start ${sessionId} recordingAvailable=${recordingAvailable}`,
134
+ sessionId,
135
+ );
136
+ },
137
+
138
+ onSpeechEvent({ sessionId }, event: SpeechEvent) {
139
+ relaySpeechEvent(sessionId, event);
140
+ },
141
+
142
+ onUserSpeechFinal({ sessionId, text }) {
143
+ void onUserText(sessionId, text);
144
+ },
145
+
146
+ onDataChannelMessage(ctx) {
147
+ const text = parseChatText(ctx.message);
148
+ if (!text) return;
149
+ void onUserText(ctx.sessionId, text);
150
+ },
151
+
152
+ onSessionEnd({ sessionId }) {
153
+ sessions.delete(sessionId);
154
+ consentBySessionId.delete(sessionId);
155
+ sendToClient(sessionId, {
156
+ type: "agent_event",
157
+ event: "session_end",
158
+ sessionId,
159
+ });
160
+ agentLog("info", `recording-consent session_end ${sessionId}`, sessionId);
161
+ },
162
+ });