infinicode 2.8.84 → 2.8.85
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 +1 -1
- package/packages/robopark/scheduler/main.py +153 -37
package/package.json
CHANGED
|
@@ -839,10 +839,70 @@ async def broadcast(event_type: str, data: dict):
|
|
|
839
839
|
ws_clients.remove(client)
|
|
840
840
|
|
|
841
841
|
# =============================================================================
|
|
842
|
-
# ROBOT ENDPOINTS
|
|
843
|
-
# =============================================================================
|
|
844
|
-
|
|
845
|
-
|
|
842
|
+
# ROBOT ENDPOINTS
|
|
843
|
+
# =============================================================================
|
|
844
|
+
|
|
845
|
+
CONNECTING_SESSION_LEASE_SECONDS = max(
|
|
846
|
+
15, int(os.getenv("ROBOPARK_CONNECTING_SESSION_LEASE_SECONDS", "45"))
|
|
847
|
+
)
|
|
848
|
+
ACTIVE_SESSION_LEASE_SECONDS = max(
|
|
849
|
+
CONNECTING_SESSION_LEASE_SECONDS,
|
|
850
|
+
int(os.getenv("ROBOPARK_ACTIVE_SESSION_LEASE_SECONDS", "180")),
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
async def _reap_robot_session_if_stale(db, robot_id: str, current_session_id: Optional[str],
|
|
855
|
+
robot_status: Optional[str] = None) -> Optional[str]:
|
|
856
|
+
"""Release an invalid or expired session pointer without operator action."""
|
|
857
|
+
if not current_session_id:
|
|
858
|
+
return None
|
|
859
|
+
|
|
860
|
+
async with db.execute(
|
|
861
|
+
"SELECT id, started_at, last_activity_at, ended_at FROM sessions WHERE id = ?",
|
|
862
|
+
(current_session_id,),
|
|
863
|
+
) as cursor:
|
|
864
|
+
session = await cursor.fetchone()
|
|
865
|
+
|
|
866
|
+
reason = None
|
|
867
|
+
if not session:
|
|
868
|
+
reason = "missing_session"
|
|
869
|
+
elif session["ended_at"]:
|
|
870
|
+
reason = "ended_session"
|
|
871
|
+
else:
|
|
872
|
+
timestamp = session["last_activity_at"] or session["started_at"]
|
|
873
|
+
try:
|
|
874
|
+
activity_age = (datetime.utcnow() - datetime.fromisoformat(timestamp)).total_seconds()
|
|
875
|
+
except (TypeError, ValueError):
|
|
876
|
+
activity_age = ACTIVE_SESSION_LEASE_SECONDS + 1
|
|
877
|
+
lease = (
|
|
878
|
+
CONNECTING_SESSION_LEASE_SECONDS
|
|
879
|
+
if robot_status == "connecting"
|
|
880
|
+
else ACTIVE_SESSION_LEASE_SECONDS
|
|
881
|
+
)
|
|
882
|
+
if activity_age >= lease:
|
|
883
|
+
reason = "stale_connecting" if robot_status == "connecting" else "stale_activity"
|
|
884
|
+
|
|
885
|
+
if not reason:
|
|
886
|
+
return None
|
|
887
|
+
|
|
888
|
+
now = datetime.utcnow().isoformat()
|
|
889
|
+
if session and not session["ended_at"]:
|
|
890
|
+
await db.execute(
|
|
891
|
+
"UPDATE sessions SET ended_at = ?, end_reason = ? WHERE id = ? AND ended_at IS NULL",
|
|
892
|
+
(now, reason, current_session_id),
|
|
893
|
+
)
|
|
894
|
+
await db.execute(
|
|
895
|
+
"UPDATE robots SET status = 'idle', current_session_id = NULL, connected_server_id = NULL "
|
|
896
|
+
"WHERE id = ? AND current_session_id = ?",
|
|
897
|
+
(robot_id, current_session_id),
|
|
898
|
+
)
|
|
899
|
+
logger.warning(
|
|
900
|
+
"Automatically released %s session %s for robot %s",
|
|
901
|
+
reason, current_session_id, robot_id,
|
|
902
|
+
)
|
|
903
|
+
return reason
|
|
904
|
+
|
|
905
|
+
@app.get("/api/robots", response_model=List[Robot])
|
|
846
906
|
async def list_robots():
|
|
847
907
|
"""List all robots"""
|
|
848
908
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
@@ -909,11 +969,23 @@ async def request_session(robot_id: str,
|
|
|
909
969
|
(robot_id, device_row["name"]),
|
|
910
970
|
)
|
|
911
971
|
await db.commit()
|
|
912
|
-
robot = {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
972
|
+
robot = {
|
|
973
|
+
"id": robot_id,
|
|
974
|
+
"name": device_row["name"],
|
|
975
|
+
"status": "idle",
|
|
976
|
+
"current_session_id": None,
|
|
977
|
+
}
|
|
978
|
+
else:
|
|
979
|
+
raise HTTPException(404, "Robot not found")
|
|
980
|
+
stale_reason = await _reap_robot_session_if_stale(
|
|
981
|
+
db, robot_id, robot["current_session_id"], robot["status"]
|
|
982
|
+
)
|
|
983
|
+
if stale_reason:
|
|
984
|
+
await db.commit()
|
|
985
|
+
async with db.execute("SELECT * FROM robots WHERE id = ?", (robot_id,)) as cursor:
|
|
986
|
+
robot = await cursor.fetchone()
|
|
987
|
+
if robot["current_session_id"] or robot["status"] != "idle":
|
|
988
|
+
raise HTTPException(409, f"Robot is {robot['status']}, not idle")
|
|
917
989
|
|
|
918
990
|
# Find least-loaded online server
|
|
919
991
|
async with db.execute("""
|
|
@@ -1046,7 +1118,7 @@ async def simulate_trigger(robot_id: str):
|
|
|
1046
1118
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
1047
1119
|
db.row_factory = aiosqlite.Row
|
|
1048
1120
|
async with db.execute(
|
|
1049
|
-
"SELECT id, current_session_id FROM robots WHERE id = ?", (robot_id,)
|
|
1121
|
+
"SELECT id, current_session_id, status FROM robots WHERE id = ?", (robot_id,)
|
|
1050
1122
|
) as c:
|
|
1051
1123
|
robot = await c.fetchone()
|
|
1052
1124
|
if not robot:
|
|
@@ -1059,11 +1131,14 @@ async def simulate_trigger(robot_id: str):
|
|
|
1059
1131
|
(device["id"], device["name"], device["character_id"]),
|
|
1060
1132
|
)
|
|
1061
1133
|
await db.commit()
|
|
1062
|
-
robot = {"id": device["id"], "current_session_id": None}
|
|
1134
|
+
robot = {"id": device["id"], "current_session_id": None, "status": "idle"}
|
|
1063
1135
|
if not await _effective_device_production_mode(db, robot_id):
|
|
1064
1136
|
return {"queued": False, "reason": "device_production_mode_off"}
|
|
1065
|
-
|
|
1066
|
-
|
|
1137
|
+
stale_reason = await _reap_robot_session_if_stale(
|
|
1138
|
+
db, robot_id, robot["current_session_id"], robot["status"]
|
|
1139
|
+
)
|
|
1140
|
+
if robot["current_session_id"] and not stale_reason:
|
|
1141
|
+
return {"queued": False, "reason": "session_active"}
|
|
1067
1142
|
await db.execute(
|
|
1068
1143
|
"INSERT INTO trigger_commands (robot_id, source, requested_at) VALUES (?, ?, ?) "
|
|
1069
1144
|
"ON CONFLICT(robot_id) DO UPDATE SET source = excluded.source, requested_at = excluded.requested_at",
|
|
@@ -1072,7 +1147,12 @@ async def simulate_trigger(robot_id: str):
|
|
|
1072
1147
|
await db.commit()
|
|
1073
1148
|
await broadcast("robot_triggered", {"robot_id": robot_id, "source": "dashboard"})
|
|
1074
1149
|
await _log_history("robot", "robot", robot_id, "simulate_trigger", "operator")
|
|
1075
|
-
return {
|
|
1150
|
+
return {
|
|
1151
|
+
"queued": True,
|
|
1152
|
+
"source": "dashboard",
|
|
1153
|
+
"recovered_stale_session": bool(stale_reason),
|
|
1154
|
+
"recovery_reason": stale_reason,
|
|
1155
|
+
}
|
|
1076
1156
|
|
|
1077
1157
|
@app.post("/api/robots/{robot_id}/simulate-stop")
|
|
1078
1158
|
async def simulate_stop(robot_id: str):
|
|
@@ -1487,14 +1567,31 @@ async def session_joined(session_id: str,
|
|
|
1487
1567
|
session = await c.fetchone()
|
|
1488
1568
|
if not session:
|
|
1489
1569
|
raise HTTPException(404, "Session not found")
|
|
1490
|
-
if session["joined_at"]:
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1570
|
+
if session["joined_at"]:
|
|
1571
|
+
if not session["ended_at"]:
|
|
1572
|
+
await db.execute(
|
|
1573
|
+
"UPDATE sessions SET last_activity_at = ? WHERE id = ?",
|
|
1574
|
+
(now, session_id),
|
|
1575
|
+
)
|
|
1576
|
+
await db.execute(
|
|
1577
|
+
"UPDATE robots SET status = 'running' WHERE id = ? AND current_session_id = ?",
|
|
1578
|
+
(session["robot_id"], session_id),
|
|
1579
|
+
)
|
|
1580
|
+
await db.commit()
|
|
1581
|
+
return {
|
|
1582
|
+
"status": "already_joined",
|
|
1583
|
+
"joined_at": session["joined_at"],
|
|
1584
|
+
"latency_ms": _session_latency_ms(session["started_at"], session["joined_at"]),
|
|
1585
|
+
}
|
|
1586
|
+
await db.execute(
|
|
1587
|
+
"UPDATE sessions SET joined_at = ?, last_activity_at = ? WHERE id = ?",
|
|
1588
|
+
(now, now, session_id),
|
|
1589
|
+
)
|
|
1590
|
+
await db.execute(
|
|
1591
|
+
"UPDATE robots SET status = 'running' WHERE id = ? AND current_session_id = ?",
|
|
1592
|
+
(session["robot_id"], session_id),
|
|
1593
|
+
)
|
|
1594
|
+
await db.commit()
|
|
1498
1595
|
latency_ms = _session_latency_ms(session["started_at"], now)
|
|
1499
1596
|
await broadcast("session_joined", {"session_id": session_id, "latency_ms": latency_ms})
|
|
1500
1597
|
await _log_history("session", "session", session_id, "joined", "robot", f"latency_ms={latency_ms}")
|
|
@@ -1955,19 +2052,38 @@ async def health_checker():
|
|
|
1955
2052
|
await db.execute("UPDATE devices SET status = 'offline' WHERE id = ?", (did,))
|
|
1956
2053
|
await broadcast("device_status_changed", {"device_id": did, "status": "offline"})
|
|
1957
2054
|
|
|
1958
|
-
#
|
|
1959
|
-
#
|
|
1960
|
-
#
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2055
|
+
# Release stale robot session pointers with the same lease rules
|
|
2056
|
+
# used by the trigger path. Failed joins cannot require manual
|
|
2057
|
+
# operator recovery or block an unattended robot indefinitely.
|
|
2058
|
+
async with db.execute(
|
|
2059
|
+
"SELECT id, status, current_session_id FROM robots WHERE current_session_id IS NOT NULL"
|
|
2060
|
+
) as c:
|
|
2061
|
+
session_owners = await c.fetchall()
|
|
2062
|
+
for owner in session_owners:
|
|
2063
|
+
reason = await _reap_robot_session_if_stale(
|
|
2064
|
+
db, owner["id"], owner["current_session_id"], owner["status"]
|
|
2065
|
+
)
|
|
2066
|
+
if reason:
|
|
2067
|
+
await broadcast("session_expired", {
|
|
2068
|
+
"session_id": owner["current_session_id"],
|
|
2069
|
+
"robot_id": owner["id"],
|
|
2070
|
+
"reason": reason,
|
|
2071
|
+
})
|
|
2072
|
+
|
|
2073
|
+
# Reap old orphan rows that are no longer owned by a robot.
|
|
2074
|
+
session_cutoff = (
|
|
2075
|
+
datetime.utcnow() - timedelta(seconds=ACTIVE_SESSION_LEASE_SECONDS)
|
|
2076
|
+
).isoformat()
|
|
2077
|
+
async with db.execute(
|
|
2078
|
+
"SELECT id, robot_id FROM sessions "
|
|
2079
|
+
"WHERE ended_at IS NULL AND COALESCE(last_activity_at, started_at) < ? "
|
|
2080
|
+
"AND NOT EXISTS (SELECT 1 FROM robots r WHERE r.current_session_id = sessions.id)",
|
|
2081
|
+
(session_cutoff,),
|
|
2082
|
+
) as c:
|
|
1967
2083
|
stale_sessions = await c.fetchall()
|
|
1968
2084
|
for session in stale_sessions:
|
|
1969
2085
|
await db.execute(
|
|
1970
|
-
"UPDATE sessions SET ended_at = ?, end_reason = '
|
|
2086
|
+
"UPDATE sessions SET ended_at = ?, end_reason = 'stale_orphan' "
|
|
1971
2087
|
"WHERE id = ? AND ended_at IS NULL",
|
|
1972
2088
|
(datetime.utcnow().isoformat(), session["id"]),
|
|
1973
2089
|
)
|
|
@@ -1979,7 +2095,7 @@ async def health_checker():
|
|
|
1979
2095
|
await broadcast("session_expired", {
|
|
1980
2096
|
"session_id": session["id"],
|
|
1981
2097
|
"robot_id": session["robot_id"],
|
|
1982
|
-
|
|
2098
|
+
"reason": "stale_orphan",
|
|
1983
2099
|
})
|
|
1984
2100
|
|
|
1985
2101
|
await db.commit()
|
|
@@ -4748,10 +4864,10 @@ async def _robot_telemetry(db, robot_id: str) -> Optional[dict]:
|
|
|
4748
4864
|
readiness_code, readiness_message = "server_unavailable", f"LiveKit server {robot['connected_server_id']} is {server_status or 'missing'}"
|
|
4749
4865
|
elif not robot["current_session_id"]:
|
|
4750
4866
|
readiness_code, readiness_message = "ready", "ready for motion trigger"
|
|
4751
|
-
elif robot["status"] == "connecting" and session_age is not None and session_age >
|
|
4752
|
-
readiness_code, readiness_message = "stuck_session", f"joining LiveKit for {session_age}s;
|
|
4753
|
-
elif activity_age is not None and activity_age >
|
|
4754
|
-
readiness_code, readiness_message = "stale_session", f"no session activity for {activity_age}s;
|
|
4867
|
+
elif robot["status"] == "connecting" and session_age is not None and session_age > CONNECTING_SESSION_LEASE_SECONDS:
|
|
4868
|
+
readiness_code, readiness_message = "stuck_session", f"joining LiveKit for {session_age}s; automatic recovery pending"
|
|
4869
|
+
elif activity_age is not None and activity_age > ACTIVE_SESSION_LEASE_SECONDS:
|
|
4870
|
+
readiness_code, readiness_message = "stale_session", f"no session activity for {activity_age}s; automatic recovery pending"
|
|
4755
4871
|
else:
|
|
4756
4872
|
readiness_code, readiness_message = "in_session", "conversation session is active"
|
|
4757
4873
|
|