@voicethere/agent 0.5.6 → 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.
@@ -11460,16 +11460,6 @@ function writeObjectSlot(world, slot, objectId, posX, posY, posZ, posW, dirX, di
11460
11460
  world[start + 7] = dirZ;
11461
11461
  world[start + 8] = dirW;
11462
11462
  }
11463
- function preserveEmptySlots(simulated, authoritative) {
11464
- for (let slot = 0; slot < MAX_LIVE_OBJECTS; slot += 1) {
11465
- if (readSlotObjectId(authoritative, slot) === 0) {
11466
- markSlotFree(simulated, slot);
11467
- }
11468
- }
11469
- }
11470
- function commitSimulatedWorld(simulated, latestRedis) {
11471
- preserveEmptySlots(simulated, latestRedis);
11472
- }
11473
11463
  function collectActiveObjectIds(world) {
11474
11464
  const ids = [];
11475
11465
  for (let slot = 0; slot < MAX_LIVE_OBJECTS; slot += 1) {
@@ -11480,6 +11470,32 @@ function collectActiveObjectIds(world) {
11480
11470
  }
11481
11471
  return ids;
11482
11472
  }
11473
+ function liveWorldSnapshot(world, owners) {
11474
+ const ids = collectActiveObjectIds(world);
11475
+ ids.sort((a, b) => a - b);
11476
+ return ids.map((objectId) => ({
11477
+ objectId,
11478
+ ownerSessionId: owners.get(objectId) ?? ""
11479
+ }));
11480
+ }
11481
+ function planRedisSimTick(options) {
11482
+ if (options.lockAcquired) {
11483
+ return "simulate";
11484
+ }
11485
+ if (options.connectedSessions.size > 0) {
11486
+ return "relay";
11487
+ }
11488
+ return "noop";
11489
+ }
11490
+ function clampSimulationDtSec(elapsedMs, fallbackHz) {
11491
+ const minDt = 1 / fallbackHz;
11492
+ const maxDt = 0.05;
11493
+ if (!Number.isFinite(elapsedMs) || elapsedMs <= 0) {
11494
+ return minDt;
11495
+ }
11496
+ const elapsedSec = elapsedMs / 1e3;
11497
+ return Math.min(maxDt, Math.max(minDt, elapsedSec));
11498
+ }
11483
11499
  function normalizeWorldBuffer(raw) {
11484
11500
  if (!raw || raw.byteLength === 0) {
11485
11501
  return createEmptyWorldBuffer();
@@ -11718,6 +11734,7 @@ var worldState = createEmptyWorldBuffer();
11718
11734
  var redis = null;
11719
11735
  var broadcastTimer = null;
11720
11736
  var worldMutationChain = Promise.resolve();
11737
+ var lastTickTimeMs = 0;
11721
11738
  function withWorldMutation(fn) {
11722
11739
  const run = worldMutationChain.then(fn);
11723
11740
  worldMutationChain = run.then(
@@ -11902,13 +11919,11 @@ async function unregisterObject(sessionId, objectId) {
11902
11919
  }
11903
11920
  return { ok: true, objectId: target.objectId };
11904
11921
  }
11905
- function trackedObjectsSnapshot() {
11906
- const objects = [];
11907
- for (const [objectId, ownerSessionId] of objectOwners) {
11908
- objects.push({ objectId, ownerSessionId });
11922
+ function broadcastWorldSnapshot() {
11923
+ const objects = liveWorldSnapshot(worldState, objectOwners);
11924
+ for (const sessionId of connectedSessions) {
11925
+ sendToClient(sessionId, { type: "world_snapshot", objects });
11909
11926
  }
11910
- objects.sort((a, b) => a.objectId - b.objectId);
11911
- return objects;
11912
11927
  }
11913
11928
  function notifyObjectRegistered(objectId, ownerSessionId) {
11914
11929
  for (const sessionId of connectedSessions) {
@@ -11952,31 +11967,44 @@ async function saveWorldToRedis(world) {
11952
11967
  await redis.setBuffer(REDIS_WORLD_KEY, copyWorldBuffer(world));
11953
11968
  }
11954
11969
  async function runSimulationTick() {
11970
+ const now = Date.now();
11971
+ const dt = clampSimulationDtSec(
11972
+ lastTickTimeMs === 0 ? 0 : now - lastTickTimeMs,
11973
+ BROADCAST_HZ
11974
+ );
11975
+ lastTickTimeMs = now;
11955
11976
  if (!redis) {
11956
11977
  await withWorldMutation(async () => {
11957
- const activeObjectIds = [...objectOwners.keys()];
11958
- simulateWorldStep(worldState, 1 / BROADCAST_HZ, activeObjectIds);
11978
+ simulateWorldStep(worldState, dt, collectActiveObjectIds(worldState));
11959
11979
  });
11960
11980
  broadcastWorldBuffer(worldState);
11961
11981
  return;
11962
11982
  }
11963
11983
  await withWorldMutation(async () => {
11964
- await withRedisSimLock(
11984
+ const lockResult = await withRedisSimLock(
11965
11985
  async () => {
11966
- const world2 = await loadWorldFromRedis();
11967
- const activeObjectIds = collectActiveObjectIds(world2);
11968
- simulateWorldStep(world2, 1 / BROADCAST_HZ, activeObjectIds);
11969
- const latestRedis = await loadWorldFromRedis();
11970
- commitSimulatedWorld(world2, latestRedis);
11971
- await saveWorldToRedis(world2);
11972
- worldState = world2;
11986
+ const world = await loadWorldFromRedis();
11987
+ const activeObjectIds = collectActiveObjectIds(world);
11988
+ simulateWorldStep(world, dt, activeObjectIds);
11989
+ await saveWorldToRedis(world);
11990
+ worldState = world;
11991
+ broadcastWorldBuffer(world);
11992
+ return true;
11973
11993
  },
11974
11994
  { retryUntilAcquired: false }
11975
11995
  );
11996
+ if (lockResult === null) {
11997
+ const plan = planRedisSimTick({
11998
+ lockAcquired: false,
11999
+ connectedSessions
12000
+ });
12001
+ if (plan === "relay") {
12002
+ const world = await loadWorldFromRedis();
12003
+ worldState = world;
12004
+ broadcastWorldBuffer(world);
12005
+ }
12006
+ }
11976
12007
  });
11977
- const world = await loadWorldFromRedis();
11978
- worldState = world;
11979
- broadcastWorldBuffer(world);
11980
12008
  }
11981
12009
  function startBroadcastLoopIfNeeded() {
11982
12010
  if (broadcastTimer) return;
@@ -12033,12 +12061,15 @@ defineAgent({
12033
12061
  startBroadcastLoopIfNeeded();
12034
12062
  agentLog("info", "game-sync agent connected to project Redis world buffer");
12035
12063
  },
12036
- onClientJoin({ sessionId }) {
12064
+ async onClientJoin({ sessionId }) {
12037
12065
  connectedSessions.add(sessionId);
12038
12066
  startBroadcastLoopIfNeeded();
12067
+ if (redis) {
12068
+ worldState = await loadWorldFromRedis();
12069
+ }
12039
12070
  sendToClient(sessionId, {
12040
12071
  type: "world_snapshot",
12041
- objects: trackedObjectsSnapshot()
12072
+ objects: liveWorldSnapshot(worldState, objectOwners)
12042
12073
  });
12043
12074
  agentLog("info", `join ${sessionId} connected=${connectedSessions.size}`);
12044
12075
  },
@@ -12081,6 +12112,7 @@ defineAgent({
12081
12112
  }
12082
12113
  sendToClient(ctx.sessionId, { type: "register_ack", objectId });
12083
12114
  notifyObjectRegistered(objectId, ctx.sessionId);
12115
+ broadcastWorldSnapshot();
12084
12116
  agentLog(
12085
12117
  "info",
12086
12118
  `register session=${ctx.sessionId} objectId=${objectId}`
@@ -12105,6 +12137,7 @@ defineAgent({
12105
12137
  type: "unregister_ack",
12106
12138
  objectId: result.objectId
12107
12139
  });
12140
+ broadcastWorldSnapshot();
12108
12141
  agentLog(
12109
12142
  "info",
12110
12143
  `unregister_ack session=${ctx.sessionId} objectId=${result.objectId}`
@@ -13,6 +13,8 @@ export declare function listTemplates(filter?: ListTemplatesFilter): AgentTempla
13
13
  export declare function getTemplate(id: string): AgentTemplateDefinition;
14
14
  export declare function resolveTemplateEntryPath(id: string): string;
15
15
  export declare function loadTemplateSources(id: string): TemplateSourceFile[];
16
+ /** Like {@link loadTemplateSources}, but paths match registry `sourceFiles` (e.g. `voice-showcase/agent.ts`). */
17
+ export declare function loadTemplateWorkspaceSources(id: string): TemplateSourceFile[];
16
18
  export declare function hasSeedBundle(id: string): boolean;
17
19
  export declare function loadTemplateBundle(id: string): Buffer;
18
20
  export declare function buildSeedTemplateBundles(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EAGlB,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,uBAAuB,EACvB,eAAe,EACf,YAAY,GACb,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,kBAAkB;IACjC,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AAaD,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,mBAAmB,GAC3B,uBAAuB,EAAE,CAK3B;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,CAE/D;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAO3D;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAYpE;AAMD,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAMjD;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAYrD;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC,CAS9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EAGlB,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,uBAAuB,EACvB,eAAe,EACf,YAAY,GACb,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,kBAAkB;IACjC,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AAaD,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,mBAAmB,GAC3B,uBAAuB,EAAE,CAK3B;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,CAE/D;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAO3D;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAYpE;AAED,iHAAiH;AACjH,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAY7E;AAMD,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAMjD;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAYrD;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC,CAS9D"}
@@ -43,6 +43,20 @@ export function loadTemplateSources(id) {
43
43
  };
44
44
  });
45
45
  }
46
+ /** Like {@link loadTemplateSources}, but paths match registry `sourceFiles` (e.g. `voice-showcase/agent.ts`). */
47
+ export function loadTemplateWorkspaceSources(id) {
48
+ const template = getTemplateById(id);
49
+ return template.sourceFiles.map((sourceFile) => {
50
+ const absolutePath = join(TEMPLATES_DIR, sourceFile);
51
+ if (!existsSync(absolutePath)) {
52
+ throw new Error(`Template source not found for ${id}: ${absolutePath}`);
53
+ }
54
+ return {
55
+ path: sourceFile,
56
+ content: readFileSync(absolutePath, "utf8"),
57
+ };
58
+ });
59
+ }
46
60
  function seedBundlePath(id) {
47
61
  return join(PACKAGE_ROOT, "dist", "templates", id, "agent.js");
48
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"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"}
@@ -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,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"}
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,6 +46,7 @@ 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",
@@ -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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"}
@@ -1317,6 +1317,36 @@ async function resolveWeatherTurn(state, city, country, fetchFn) {
1317
1317
  }
1318
1318
  }
1319
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
+
1320
1350
  // templates/voice-showcase/agent.ts
1321
1351
  var sessions = /* @__PURE__ */ new Map();
1322
1352
  function getState(sessionId) {
@@ -1335,20 +1365,16 @@ function relaySpeechEvent(sessionId, event) {
1335
1365
  raw: event
1336
1366
  });
1337
1367
  }
1338
- function deliverMessages(sessionId, messages) {
1339
- for (const message of messages) {
1340
- sendToClient(sessionId, message);
1341
- }
1342
- }
1343
- function speakLines(sessionId, lines) {
1344
- for (const line of lines) {
1345
- speak(sessionId, line);
1346
- }
1368
+ function deliverSpokenThenPlay(sessionId, result) {
1369
+ applyOutboundOps(
1370
+ sessionId,
1371
+ spokenThenPlayOps(result.messages, result.speakLines),
1372
+ { sendToClient, speak }
1373
+ );
1347
1374
  }
1348
1375
  async function applyTurn(sessionId, result) {
1349
1376
  sessions.set(sessionId, result.state);
1350
- speakLines(sessionId, result.speakLines);
1351
- deliverMessages(sessionId, result.messages);
1377
+ deliverSpokenThenPlay(sessionId, result);
1352
1378
  if (result.pendingWeather) {
1353
1379
  const weatherResult = await resolveWeatherTurn(
1354
1380
  result.state,
@@ -1356,8 +1382,7 @@ async function applyTurn(sessionId, result) {
1356
1382
  result.pendingWeather.country
1357
1383
  );
1358
1384
  sessions.set(sessionId, weatherResult.state);
1359
- speakLines(sessionId, weatherResult.speakLines);
1360
- deliverMessages(sessionId, weatherResult.messages);
1385
+ deliverSpokenThenPlay(sessionId, weatherResult);
1361
1386
  }
1362
1387
  }
1363
1388
  async function onUserText(sessionId, text) {
@@ -1368,13 +1393,10 @@ async function onUserText(sessionId, text) {
1368
1393
  defineAgent({
1369
1394
  onSessionStart({ sessionId }) {
1370
1395
  sessions.set(sessionId, createInitialState());
1371
- sendToClient(sessionId, {
1372
- type: "agent_event",
1373
- event: "session_start",
1374
- sessionId
1396
+ applyOutboundOps(sessionId, greetingOps(sessionId, GREETING), {
1397
+ sendToClient,
1398
+ speak
1375
1399
  });
1376
- speak(sessionId, GREETING);
1377
- sendToClient(sessionId, { type: "chat_reply", text: GREETING });
1378
1400
  agentLog("info", `voice-showcase session_start ${sessionId}`);
1379
1401
  },
1380
1402
  onSpeechEvent({ sessionId }, event) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voicethere/agent",
3
- "version": "0.5.6",
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",
@@ -63,9 +63,9 @@ 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
69
 
70
70
  ### `recording-consent/` (`recording-consent`)
71
71
 
@@ -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}`,
@@ -19,8 +19,13 @@ import {
19
19
  handleUtterance,
20
20
  resolveWeatherTurn,
21
21
  type ConversationState,
22
- type OutboundMessage,
22
+ type ConversationTurnResult,
23
23
  } from "./conversation.js";
24
+ import {
25
+ applyOutboundOps,
26
+ greetingOps,
27
+ spokenThenPlayOps,
28
+ } from "./delivery.js";
24
29
 
25
30
  const sessions = new Map<string, ConversationState>();
26
31
 
@@ -42,25 +47,23 @@ function relaySpeechEvent(sessionId: string, event: SpeechEvent): void {
42
47
  });
43
48
  }
44
49
 
45
- function deliverMessages(sessionId: string, messages: OutboundMessage[]): void {
46
- for (const message of messages) {
47
- sendToClient(sessionId, message);
48
- }
49
- }
50
-
51
- function speakLines(sessionId: string, lines: string[]): void {
52
- for (const line of lines) {
53
- speak(sessionId, line);
54
- }
50
+ function deliverSpokenThenPlay(
51
+ sessionId: string,
52
+ result: Pick<ConversationTurnResult, "messages" | "speakLines">,
53
+ ): void {
54
+ applyOutboundOps(
55
+ sessionId,
56
+ spokenThenPlayOps(result.messages, result.speakLines),
57
+ { sendToClient, speak },
58
+ );
55
59
  }
56
60
 
57
61
  async function applyTurn(
58
62
  sessionId: string,
59
- result: Awaited<ReturnType<typeof handleUtterance>>,
63
+ result: ConversationTurnResult,
60
64
  ): Promise<void> {
61
65
  sessions.set(sessionId, result.state);
62
- speakLines(sessionId, result.speakLines);
63
- deliverMessages(sessionId, result.messages);
66
+ deliverSpokenThenPlay(sessionId, result);
64
67
 
65
68
  if (result.pendingWeather) {
66
69
  const weatherResult = await resolveWeatherTurn(
@@ -69,8 +72,7 @@ async function applyTurn(
69
72
  result.pendingWeather.country,
70
73
  );
71
74
  sessions.set(sessionId, weatherResult.state);
72
- speakLines(sessionId, weatherResult.speakLines);
73
- deliverMessages(sessionId, weatherResult.messages);
75
+ deliverSpokenThenPlay(sessionId, weatherResult);
74
76
  }
75
77
  }
76
78
 
@@ -83,13 +85,10 @@ async function onUserText(sessionId: string, text: string): Promise<void> {
83
85
  defineAgent({
84
86
  onSessionStart({ sessionId }) {
85
87
  sessions.set(sessionId, createInitialState());
86
- sendToClient(sessionId, {
87
- type: "agent_event",
88
- event: "session_start",
89
- sessionId,
88
+ applyOutboundOps(sessionId, greetingOps(sessionId, GREETING), {
89
+ sendToClient,
90
+ speak,
90
91
  });
91
- speak(sessionId, GREETING);
92
- sendToClient(sessionId, { type: "chat_reply", text: GREETING });
93
92
  agentLog("info", `voice-showcase session_start ${sessionId}`);
94
93
  },
95
94
 
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Voice-showcase outbound order: send spoken TTS text to the client first,
3
+ * then trigger TTS play. Parent IPC preserves this order.
4
+ */
5
+
6
+ import type { OutboundMessage } from "./conversation.js";
7
+
8
+ export type OutboundOp =
9
+ | { kind: "send"; message: OutboundMessage }
10
+ | { kind: "play"; text: string };
11
+
12
+ export interface OutboundDeps {
13
+ sendToClient: (sessionId: string, payload: unknown) => void;
14
+ speak: (sessionId: string, text: string) => void;
15
+ }
16
+
17
+ /** All DataChannel payloads, then TTS play commands. */
18
+ export function spokenThenPlayOps(
19
+ messages: OutboundMessage[],
20
+ speakLines: string[],
21
+ ): OutboundOp[] {
22
+ const ops: OutboundOp[] = [];
23
+ for (const message of messages) {
24
+ ops.push({ kind: "send", message });
25
+ }
26
+ for (const text of speakLines) {
27
+ ops.push({ kind: "play", text });
28
+ }
29
+ return ops;
30
+ }
31
+
32
+ export function greetingOps(
33
+ sessionId: string,
34
+ greeting: string,
35
+ ): OutboundOp[] {
36
+ return spokenThenPlayOps(
37
+ [
38
+ { type: "agent_event", event: "session_start", sessionId },
39
+ { type: "chat_reply", text: greeting },
40
+ ],
41
+ [greeting],
42
+ );
43
+ }
44
+
45
+ export function applyOutboundOps(
46
+ sessionId: string,
47
+ ops: OutboundOp[],
48
+ deps: OutboundDeps,
49
+ ): void {
50
+ for (const op of ops) {
51
+ if (op.kind === "send") {
52
+ deps.sendToClient(sessionId, op.message);
53
+ } else {
54
+ deps.speak(sessionId, op.text);
55
+ }
56
+ }
57
+ }