ticlawk 0.1.17-dev.23 → 0.1.17-dev.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticlawk",
3
- "version": "0.1.17-dev.23",
3
+ "version": "0.1.17-dev.24",
4
4
  "description": "Local connector that links agent harnesses (Claude Code, Codex, OpenClaw, opencode, Pi) to the Ticlawk mobile app.",
5
5
  "type": "module",
6
6
  "main": "ticlawk.mjs",
@@ -115,7 +115,10 @@ async function apiFetch(path, opts = {}) {
115
115
  // ── Agents ──
116
116
 
117
117
  export async function getAgents({ hostId } = {}) {
118
- const query = hostId ? `?${new URLSearchParams({ runtime_host_id: hostId })}` : '';
118
+ const params = new URLSearchParams();
119
+ if (hostId) params.set('runtime_host_id', hostId);
120
+ params.set('ticlawk_version', getTiclawkVersion());
121
+ const query = `?${params}`;
119
122
  const { data } = await apiFetch(`/api/agents${query}`);
120
123
  if (!Array.isArray(data)) {
121
124
  const err = new Error('API /api/agents response missing data array');
@@ -584,14 +587,23 @@ export async function getMe() {
584
587
  return data || null;
585
588
  }
586
589
 
590
+ export async function checkConnectorReadiness({ hostId }) {
591
+ return apiFetch('/api/connector/readiness', {
592
+ method: 'POST',
593
+ body: JSON.stringify(withTiclawkVersion({
594
+ runtime_host_id: hostId,
595
+ })),
596
+ });
597
+ }
598
+
587
599
  export async function reportHostCapabilities({ hostId, hostLabel, runtimesHealth, daemonVersion }) {
588
600
  return apiFetch('/api/hosts/capabilities', {
589
601
  method: 'POST',
590
- body: JSON.stringify({
602
+ body: JSON.stringify(withTiclawkVersion({
591
603
  host_id: hostId,
592
604
  host_label: hostLabel || null,
593
605
  runtimes_health: runtimesHealth || {},
594
- daemon_version: daemonVersion || null,
595
- }),
606
+ daemon_version: daemonVersion || getTiclawkVersion(),
607
+ })),
596
608
  });
597
609
  }
@@ -16,6 +16,7 @@ const require = createRequire(import.meta.url);
16
16
  const qrcode = require('qrcode-terminal');
17
17
  const JOBS_WAKE_DEBOUNCE_MS = 100;
18
18
  const BINDINGS_WAKE_DEBOUNCE_MS = 500;
19
+ const READINESS_AUDIT_INTERVAL_MS = 60 * 1000;
19
20
  const RECOVERY_AUDIT_INTERVAL_MS = 5 * 60 * 1000;
20
21
  const BINDING_AUDIT_INTERVAL_MS = 5 * 60 * 1000;
21
22
  const UPDATE_RETRY_COOLDOWN_MS = 15 * 60 * 1000;
@@ -286,6 +287,7 @@ export function createTiclawkAdapter(ctx) {
286
287
  let bindingsWakeTimer = null;
287
288
  let lastJobsWakeAt = 0;
288
289
  let lastBindingsWakeAt = 0;
290
+ let lastReadinessCheckAt = 0;
289
291
  let updateRequired = null;
290
292
  let lastUpdateRequiredLogAt = 0;
291
293
  let startupSyncing = false;
@@ -369,6 +371,7 @@ export function createTiclawkAdapter(ctx) {
369
371
  autoUpdateError,
370
372
  lastAutoUpdateAttemptAt,
371
373
  });
374
+ stopWakeSocket('update_required');
372
375
 
373
376
  if (now - lastUpdateRequiredLogAt > UPDATE_REQUIRED_LOG_INTERVAL_MS) {
374
377
  lastUpdateRequiredLogAt = now;
@@ -732,6 +735,35 @@ export function createTiclawkAdapter(ctx) {
732
735
  bindingsWakeTimer.unref?.();
733
736
  }
734
737
 
738
+ async function checkConnectorReadiness(reason, { force = false } = {}) {
739
+ const now = Date.now();
740
+ if (!force && now - lastReadinessCheckAt < READINESS_AUDIT_INTERVAL_MS) {
741
+ return true;
742
+ }
743
+ lastReadinessCheckAt = now;
744
+ try {
745
+ await api.checkConnectorReadiness({ hostId });
746
+ clearUpdateRequired(`readiness.${reason}`);
747
+ debugLog('ticlawk', 'readiness.ok', {
748
+ reason,
749
+ hostId,
750
+ ticlawkVersion: api.getTiclawkVersion(),
751
+ });
752
+ return true;
753
+ } catch (err) {
754
+ if (api.isUpdateRequiredError(err)) {
755
+ recordUpdateRequired(err, `readiness.${reason}`);
756
+ return false;
757
+ }
758
+ debugError('ticlawk', 'readiness.failed', {
759
+ reason,
760
+ hostId,
761
+ error: err?.message || 'readiness check failed',
762
+ });
763
+ return true;
764
+ }
765
+ }
766
+
735
767
  async function reportHostCapabilitiesNow() {
736
768
  const entries = await Promise.all(Object.entries(ctx.runtimes || {})
737
769
  .filter(([, runtime]) => typeof runtime?.health === 'function')
@@ -757,22 +789,39 @@ export function createTiclawkAdapter(ctx) {
757
789
  .map(([k]) => k)
758
790
  .join(','),
759
791
  });
792
+ clearUpdateRequired('host.capabilities');
793
+ return true;
760
794
  } catch (err) {
795
+ if (api.isUpdateRequiredError(err)) {
796
+ recordUpdateRequired(err, 'host.capabilities');
797
+ return false;
798
+ }
761
799
  debugError('ticlawk', 'host.capabilities.failed', {
762
800
  hostId,
763
801
  error: err?.message || 'report failed',
764
802
  });
803
+ return true;
765
804
  }
766
805
  }
767
806
 
807
+ async function handleWakeHello() {
808
+ const ready = await checkConnectorReadiness('wake.hello', { force: true });
809
+ if (!ready) return;
810
+ const capabilitiesReported = await reportHostCapabilitiesNow();
811
+ if (!capabilitiesReported) return;
812
+ if (startupSyncing) return;
813
+ await refreshBindings('wake.hello');
814
+ await requestDrain('wake.hello');
815
+ }
816
+
768
817
  function handleWakeEvent(event) {
769
818
  wakeState.lastEventAt = new Date().toISOString();
770
819
  if (event?.type === 'hello') {
771
- void reportHostCapabilitiesNow();
772
- if (startupSyncing) return;
773
- void refreshBindings('wake.hello')
774
- .then(() => requestDrain('wake.hello'))
775
- .catch(() => {});
820
+ void handleWakeHello().catch(() => {});
821
+ return;
822
+ }
823
+ if (event?.type === 'heartbeat') {
824
+ void checkConnectorReadiness('wake.heartbeat').catch(() => {});
776
825
  return;
777
826
  }
778
827
  if (event?.type === 'jobs.available') {
@@ -848,6 +897,13 @@ export function createTiclawkAdapter(ctx) {
848
897
  connectorSocket.start();
849
898
  }
850
899
 
900
+ function stopWakeSocket(reason) {
901
+ if (!connectorSocket) return;
902
+ connectorSocket.stop();
903
+ connectorSocket = null;
904
+ debugLog('ticlawk-wake', 'stopped', { reason });
905
+ }
906
+
851
907
  function restartWakeSocket(reason) {
852
908
  if (connectorSocket) {
853
909
  connectorSocket.stop();
@@ -882,11 +938,14 @@ export function createTiclawkAdapter(ctx) {
882
938
  meta: pairedIdentity,
883
939
  });
884
940
  }
885
- if (connectorSocket) {
941
+ const ready = await checkConnectorReadiness('connect.paired', { force: true });
942
+ if (connectorSocket && ready) {
886
943
  restartWakeSocket('connect.paired');
887
944
  }
888
945
 
889
- await reportHostCapabilitiesNow();
946
+ if (ready) {
947
+ await reportHostCapabilitiesNow();
948
+ }
890
949
  return {
891
950
  statusCode: 200,
892
951
  body: {
@@ -948,10 +1007,13 @@ export function createTiclawkAdapter(ctx) {
948
1007
  // was dropped in X1; no cron has replaced it. Rare in practice;
949
1008
  // when it happens the fix is a one-row UPDATE in supabase.
950
1009
  startupSyncing = true;
951
- connectWakeSocket();
952
1010
  try {
953
- await refreshBindings('startup');
954
- await requestDrain('startup');
1011
+ const ready = await checkConnectorReadiness('startup', { force: true });
1012
+ if (ready) {
1013
+ connectWakeSocket();
1014
+ await refreshBindings('startup');
1015
+ await requestDrain('startup');
1016
+ }
955
1017
  } finally {
956
1018
  startupSyncing = false;
957
1019
  }